//! Native `TestClient` shell, registry, and first-wave command groups.

mod inventory;
mod services;
mod voice;
mod world;

use crate::commands::TEST_CLIENT_COMMANDS;
use clap::Parser;
use libremetaverse::packets::PacketType;
use libremetaverse::types::compat::{CancellationToken, CancellationTokenSource, Subscription};
use libremetaverse::types::{UUID, Utils};
use libremetaverse::{
    AgentManager, ChatAudibleLevel, ChatEventArgs, ChatType, DirectoryManager,
    DisconnectedEventArgs, GridClient, InstantMessageDialog, InstantMessageEventArgs,
    NetworkManager, PacketReceivedEventArgs,
};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::fs::File;
use std::future::Future;
use std::io::{self, BufRead, BufReader, BufWriter, Read, Write};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::process::ExitCode;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncBufReadExt, BufReader as AsyncBufReader};
use tokio::sync::{mpsc, oneshot};

pub const EXIT_SUCCESS: u8 = 0;
pub const EXIT_USAGE: u8 = 2;
pub const EXIT_INPUT: u8 = 3;
pub const EXIT_CLIENT: u8 = 4;

const MAX_INPUT_BYTES: u64 = 1024 * 1024;
const MAX_INPUT_LINES: usize = 4096;
const MAX_CLIENTS: usize = 64;
const MAX_COMMAND_BYTES: usize = 4096;
const MAX_MESSAGE_BYTES: usize = 1023;
const EVENT_QUEUE_CAPACITY: usize = 16_384;
const MAX_PACKET_LOG_COUNT: usize = 10_000;
const MAX_PACKET_LOG_BYTES: u64 = 16 * 1024 * 1024;
const MAX_LOADED_COMMANDS: usize = 128;
const MAX_ALIAS_DEPTH: usize = 8;

pub const IMPLEMENTED_TEST_CLIENT_COMMANDS: &[&str] = &[
    "AppearanceCommand",
    "AttachmentsCommand",
    "AvatarInfoCommand",
    "CloneCommand",
    "WearCommand",
    "BackupCommand",
    "BalanceCommand",
    "ChangeDirectoryCommand",
    "CreateNotecardCommand",
    "DeleteFolderCommand",
    "DownloadCommand",
    "DumpOutfitCommand",
    "EmptyLostAndFound",
    "EmptyTrashCommand",
    "GiveAllCommand",
    "GiveItemCommand",
    "InventoryCommand",
    "ListContentsCommand",
    "ObjectInventoryCommand",
    "ScriptCommand",
    "TaskRunningCommand",
    "UploadImageCommand",
    "UploadScriptCommand",
    "ViewNotecardCommand",
    "XferCommand",
    "DownloadTerrainCommand",
    "GetEstateCovenantCommand",
    "UploadRawTerrainCommand",
    "AgentLocationsCommand",
    "FindSimCommand",
    "GridLayerCommand",
    "GridMapCommand",
    "ParcelDetailsCommand",
    "ParcelInfoCommand",
    "ParcelPrimOwnersCommand",
    "ParcelSelectObjectsCommand",
    "SyntaxIdCommand",
    "WindCommand",
    "BackCommand",
    "CrossRegionCommand",
    "CrouchCommand",
    "FlyCommand",
    "FlyToCommand",
    "FollowCommand",
    "ForwardCommand",
    "GoHome",
    "GotoCommand",
    "GotoLandmark",
    "JumpCommand",
    "LeftCommand",
    "LocationCommand",
    "MoveToCommand",
    "RightCommand",
    "SetHome",
    "SitCommand",
    "SitOnCommand",
    "StandCommand",
    "TurnToCommand",
    "ChangePermsCommand",
    "DeRezObjectCommand",
    "DownloadTextureCommand",
    "ExportCommand",
    "ExportParticlesCommand",
    "FindObjectsCommand",
    "FindTextureCommand",
    "ImportCommand",
    "PrimCountCommand",
    "PrimInfoCommand",
    "PrimRegexCommand",
    "TexturesCommand",
    "TreeCommand",
    "EchoMasterCommand",
    "IMCommand",
    "IMGroupCommand",
    "SayCommand",
    "ShoutCommand",
    "WhisperCommand",
    "AtCommand",
    "DebugCommand",
    "HelpCommand",
    "LoadCommand",
    "LogPacketCommand",
    "LoginCommand",
    "LogoutCommand",
    "MD5Command",
    "QuitCommand",
    "SetMasterCommand",
    "SetMasterKeyCommand",
    "ShowEffectsCommand",
    "SleepCommand",
    "WaitForLoginCommand",
    "BotsCommand",
    "CloneProfileCommand",
    "GenericMessageCommand",
    "PlayAnimationCommand",
    "TouchCommand",
    "WhoCommand",
    "Key2NameCommand",
    "SearchClassifiedsCommand",
    "SearchEventsCommand",
    "SearchGroupsCommand",
    "SearchLandCommand",
    "SearchPeopleCommand",
    "SearchPlacesCommand",
    "ShowEventDetailsCommand",
    "FriendsCommand",
    "MapFriendCommand",
    "ActivateGroupCommand",
    "GroupMembersCommand",
    "GroupRolesCommand",
    "GroupsCommand",
    "InviteGroupCommand",
    "JoinGroupCommand",
    "LeaveGroupCommand",
    "DilationCommand",
    "NetstatsCommand",
    "RegionInfoCommand",
    "StatsCommand",
    "UptimeCommand",
    "ParcelVoiceInfo",
    "VoiceAcountCommand",
];

#[must_use]
pub fn pending_test_client_commands() -> Vec<&'static str> {
    let implemented: HashSet<_> = IMPLEMENTED_TEST_CLIENT_COMMANDS.iter().copied().collect();
    TEST_CLIENT_COMMANDS
        .iter()
        .copied()
        .filter(|name| !implemented.contains(name))
        .collect()
}

#[must_use]
pub fn registered_test_client_command_count() -> usize {
    built_in_commands().len()
}

#[derive(Parser)]
#[command(
    name = "test-client",
    version,
    about = "Run the native LibreMetaverse multi-client command shell",
    long_about = None,
    after_help = "Owned commands include @, debug, echomaster, help, im, imgroup, load, login, logpacket, logout, md5, quit, say, setmaster, setmasterkey, shout, showeffects, sleep, waitforlogin, and whisper. All non-voice inventory, appearance, asset, movement, object, parcel, estate, grid, social, directory, and statistics commands are also native. Run `help` or `help COMMAND` inside the shell for the complete command registry."
)]
#[allow(clippy::struct_excessive_bools)]
struct Cli {
    /// First name for one initial account.
    #[arg(long)]
    first: Option<String>,
    /// Last name for one initial account.
    #[arg(long)]
    last: Option<String>,
    /// Password for one initial account. Never printed or persisted.
    #[arg(long)]
    pass: Option<String>,
    /// Bounded account list: `First Last Password [Region/x/y/z]`.
    #[arg(long, value_name = "FILE")]
    file: Option<PathBuf>,
    /// Grid login endpoint. `GRID_LOGIN_URL` is the fallback.
    #[arg(long)]
    loginuri: Option<String>,
    /// Initial location in `Region/x/y/z` form.
    #[arg(long)]
    startpos: Option<String>,
    /// Master avatar name.
    #[arg(long)]
    master: Option<String>,
    /// Master avatar UUID.
    #[arg(long)]
    masterkey: Option<String>,
    /// Permit commands from known members of joined group-chat sessions.
    #[arg(long)]
    groupcommands: bool,
    /// Automatically fetch newly observed object textures into the asset cache.
    #[arg(long)]
    gettextures: bool,
    /// Run bounded terminal commands from a file before the interactive loop.
    #[arg(long, value_name = "FILE")]
    scriptfile: Option<PathBuf>,
    /// Do not read interactive commands; wait for Ctrl-C after startup commands.
    #[arg(long)]
    nogui: bool,
    /// Run a deterministic fake-grid terminal script and exit.
    #[arg(long, value_name = "FILE")]
    fake_script: Option<PathBuf>,
    /// Maximum time allowed for each live login.
    #[arg(long, default_value_t = 30, value_name = "SECONDS", value_parser = clap::value_parser!(u64).range(1..=300))]
    login_timeout_seconds: u64,

    /// Permit commands that mutate live inventory or appearance state.
    #[arg(long)]
    allow_live_mutations: bool,

    /// Permit commands that spend or transfer L$; each command also requires confirmation.
    #[arg(long)]
    allow_spending: bool,

    /// Permit estate-owner commands; mutations still require their other gates.
    #[arg(long)]
    allow_estate_actions: bool,
}

#[derive(Debug)]
enum ProgramError {
    Usage(&'static str),
    Input { action: String, source: io::Error },
    InvalidInput { line: usize, reason: &'static str },
    Client(String),
    Signal,
}

impl ProgramError {
    const fn exit_code(&self) -> u8 {
        match self {
            Self::Usage(_) => EXIT_USAGE,
            Self::Input { .. } | Self::InvalidInput { .. } => EXIT_INPUT,
            Self::Client(_) | Self::Signal => EXIT_CLIENT,
        }
    }
}

impl fmt::Display for ProgramError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Usage(message) => f.write_str(message),
            Self::Input { action, source } => write!(f, "{action}: {source}"),
            Self::InvalidInput { line, reason } => {
                write!(f, "invalid input at line {line}: {reason}")
            }
            Self::Client(message) => f.write_str(message),
            Self::Signal => f.write_str("could not install the Ctrl-C handler"),
        }
    }
}

#[derive(Clone)]
struct Account {
    first: String,
    last: String,
    password: String,
    start: Option<String>,
    uri: Option<String>,
    master_name: String,
    master_key: UUID,
    group_commands: bool,
}

#[derive(Clone)]
enum ClientEvent {
    Chat {
        client: UUID,
        source: UUID,
        name: String,
        message: String,
        normal: bool,
        fully_audible: bool,
    },
    InstantMessage {
        client: UUID,
        source: UUID,
        name: String,
        message: String,
        dialog: InstantMessageDialog,
        session: UUID,
        group: bool,
    },
    Packet {
        client: UUID,
        packet_type: String,
        simulator: String,
        bytes: usize,
    },
    Effect {
        client: UUID,
        summary: String,
    },
    Disconnected {
        client: UUID,
        reason: String,
    },
}

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

trait ClientBackend:
    Send + Sync + inventory::Backend + services::Backend + voice::Backend + world::Backend
{
    fn id(&self) -> UUID;
    fn name(&self) -> String;
    fn connected(&self) -> bool;
    fn chat(&self, message: &str, channel: i32, kind: ChatType) -> Result<(), &'static str>;
    fn instant_message(&self, target: UUID, message: &str) -> Result<(), &'static str>;
    fn group_message<'a>(
        &'a self,
        group: UUID,
        message: &'a str,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<(), &'static str>>;
    fn resolve_avatar<'a>(
        &'a self,
        name: &'a str,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<Option<UUID>, &'static str>>;
    fn accept_teleport(&self, source: UUID, session: UUID) -> Result<(), &'static str>;
    fn pause(&self) -> Result<(), &'static str>;
    fn resume(&self) -> Result<(), &'static str>;
    fn is_group_member(&self, id: UUID) -> bool;
    fn shutdown(&self);
    fn take_calls(&self) -> Vec<String> {
        Vec::new()
    }
}

struct LiveBackend {
    client: Mutex<GridClient>,
    network: NetworkManager,
    directory: DirectoryManager,
    id: UUID,
    name: String,
    subscriptions: Mutex<Vec<Subscription>>,
    inventory_state: Mutex<inventory::State>,
    world_state: Mutex<world::State>,
    services_state: Mutex<services::State>,
    closed: AtomicBool,
}

impl LiveBackend {
    async fn login(
        mut account: Account,
        timeout: Duration,
        events: mpsc::Sender<ClientEvent>,
        dropped: Arc<AtomicUsize>,
        account_policy: inventory::MutationPolicy,
        get_textures: bool,
        cancellation: CancellationToken,
    ) -> Result<Arc<Self>, ProgramError> {
        let mut client = GridClient::new()
            .map_err(|_| ProgramError::Client("could not construct GridClient".into()))?;
        let name = format!("{} {}", account.first, account.last);
        let network = client.network();
        let directory = client.directory();
        let _inventory = client.inventory();
        let _ = client.self_();
        let mut login = network
            .default_login_params(
                std::mem::take(&mut account.first),
                std::mem::take(&mut account.last),
                std::mem::take(&mut account.password),
                "TestClient".into(),
                env!("CARGO_PKG_VERSION").into(),
            )
            .map_err(|_| ProgramError::Client("could not build login parameters".into()))?;
        if let Some(start) = account.start.take() {
            login.start = start;
        }
        if let Some(uri) = account.uri.take() {
            login.uri = uri;
        }
        let login_cancel = CancellationTokenSource::new();
        let result = tokio::select! {
            () = cancellation.cancelled() => {
                login_cancel.cancel();
                let _ = network.abort_login();
                return Err(ProgramError::Client("login cancelled".into()));
            },
            () = tokio::time::sleep(timeout) => {
                login_cancel.cancel();
                let _ = network.abort_login();
                return Err(ProgramError::Client("login timed out".into()));
            },
            result = network.login_with_login_params_cancellation_token(login, Some(login_cancel.token())) => result,
        }
        .map_err(|_| ProgramError::Client("grid login failed".into()))?;
        if !result {
            return Err(ProgramError::Client("grid login was rejected".into()));
        }
        let id = client.self_().agent_id();
        let backend = Arc::new(Self {
            client: Mutex::new(client),
            network,
            directory,
            id,
            name,
            subscriptions: Mutex::new(Vec::new()),
            inventory_state: Mutex::new(inventory::State::new(account_policy)),
            world_state: Mutex::new(world::State::new(account_policy, get_textures)),
            services_state: Mutex::new(services::State::new(account_policy)),
            closed: AtomicBool::new(false),
        });
        backend.install(&events, &dropped);
        Ok(backend)
    }

    fn with_agent<T>(&self, action: impl FnOnce(&mut AgentManager) -> T) -> T {
        action(lock(&self.client).self_())
    }

    #[allow(clippy::too_many_lines)]
    fn install(self: &Arc<Self>, events: &mpsc::Sender<ClientEvent>, dropped: &Arc<AtomicUsize>) {
        let mut subscriptions = Vec::new();
        let sender = events.clone();
        let dropped_chat = Arc::clone(dropped);
        let id = self.id;
        subscriptions.push(self.with_agent(|agent| {
            agent.subscribe_chat_from_simulator(Arc::new(move |event: ChatEventArgs| {
                try_event(
                    &sender,
                    ClientEvent::Chat {
                        client: id,
                        source: event.source_id(),
                        name: event.from_name(),
                        message: event.message(),
                        normal: event.type_() == ChatType::Normal,
                        fully_audible: event.audible_level() == ChatAudibleLevel::Fully,
                    },
                    &dropped_chat,
                );
            }))
        }));
        let sender = events.clone();
        let dropped_im = Arc::clone(dropped);
        subscriptions.push(self.with_agent(|agent| {
            agent.subscribe_im(Arc::new(move |event: InstantMessageEventArgs| {
                let im = event.im();
                try_event(
                    &sender,
                    ClientEvent::InstantMessage {
                        client: id,
                        source: im.from_agent_id,
                        name: im.from_agent_name,
                        message: im.message,
                        dialog: im.dialog,
                        session: im.im_session_id,
                        group: im.group_im,
                    },
                    &dropped_im,
                );
            }))
        }));
        let sender = events.clone();
        let dropped_packet = Arc::clone(dropped);
        subscriptions.push(self.network.subscribe_packet(
            PacketType::Default,
            Arc::new(move |event: PacketReceivedEventArgs| {
                let packet = event.packet();
                let bytes = event.raw_data().map_or(0, |data| data.len());
                try_event(
                    &sender,
                    ClientEvent::Packet {
                        client: id,
                        packet_type: format!("{:?}", packet.type_),
                        simulator: event.simulator().name.clone(),
                        bytes,
                    },
                    &dropped_packet,
                );
            }),
            false,
        ));
        let sender = events.clone();
        let dropped_effect = Arc::clone(dropped);
        subscriptions.push(self.network.subscribe_packet(
            PacketType::ViewerEffect,
            Arc::new(move |event: PacketReceivedEventArgs| {
                try_event(
                    &sender,
                    ClientEvent::Effect {
                        client: id,
                        summary: format!(
                            "ViewerEffect simulator={} bytes={}",
                            event.simulator().name.clone(),
                            event.raw_data().map_or(0, |data| data.len())
                        ),
                    },
                    &dropped_effect,
                );
            }),
            false,
        ));
        let appearance_backend = Arc::downgrade(self);
        subscriptions.push(self.network.subscribe_packet(
            PacketType::AvatarAppearance,
            Arc::new(move |event: PacketReceivedEventArgs| {
                if let (Some(backend), Some(bytes)) =
                    (appearance_backend.upgrade(), event.raw_data())
                {
                    inventory::capture_appearance(&backend, &bytes);
                }
            }),
            false,
        ));
        let sender = events.clone();
        let dropped_disconnect = Arc::clone(dropped);
        subscriptions.push(self.network.subscribe_disconnected(Arc::new(
            move |event: DisconnectedEventArgs| {
                try_event(
                    &sender,
                    ClientEvent::Disconnected {
                        client: id,
                        reason: format!("{:?}: {}", event.reason(), event.message()),
                    },
                    &dropped_disconnect,
                );
            },
        )));
        world::install_live(self, &mut subscriptions);
        services::install_live(self, &mut subscriptions);
        *lock(&self.subscriptions) = subscriptions;
    }
}

impl ClientBackend for LiveBackend {
    fn id(&self) -> UUID {
        self.id
    }

    fn name(&self) -> String {
        self.name.clone()
    }

    fn connected(&self) -> bool {
        !self.closed.load(Ordering::Acquire) && self.network.connected()
    }

    fn chat(&self, message: &str, channel: i32, kind: ChatType) -> Result<(), &'static str> {
        self.with_agent(|agent| agent.chat(message.into(), channel, kind, Some(false)))
            .map_err(|_| "send local chat")
    }

    fn instant_message(&self, target: UUID, message: &str) -> Result<(), &'static str> {
        self.with_agent(|agent| agent.instant_message_with_uuid_string(target, message.into()))
            .map_err(|_| "send instant message")
    }

    fn group_message<'a>(
        &'a self,
        group: UUID,
        message: &'a str,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<(), &'static str>> {
        Box::pin(async move {
            let already_joined = self.with_agent(|agent| {
                agent
                    .group_chat_sessions
                    .read()
                    .unwrap_or_else(std::sync::PoisonError::into_inner)
                    .contains_key(&group)
            });
            if !already_joined {
                let (sender, receiver) = oneshot::channel();
                let sender = Arc::new(Mutex::new(Some(sender)));
                let callback_sender = Arc::clone(&sender);
                let subscription = self.with_agent(|agent| {
                    agent.subscribe_group_chat_joined(Arc::new(move |event| {
                        if event.session_id() == group
                            && let Some(sender) = lock(&callback_sender).take()
                        {
                            let _ = sender.send(event.success());
                        }
                    }))
                });
                self.with_agent(|agent| agent.request_join_group_chat(group))
                    .map_err(|_| "request group-chat join")?;
                let joined = tokio::select! {
                    () = cancellation.cancelled() => return Err("group-chat join cancelled"),
                    () = tokio::time::sleep(Duration::from_secs(20)) => return Err("group-chat join timed out"),
                    result = receiver => result.unwrap_or(false),
                };
                drop(subscription);
                if !joined {
                    return Err("group-chat join failed");
                }
            }
            self.with_agent(|agent| {
                agent.instant_message_group_with_uuid_string(group, message.into())
            })
            .map_err(|_| "send group instant message")
        })
    }

    fn resolve_avatar<'a>(
        &'a self,
        name: &'a str,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<Option<UUID>, &'static str>> {
        Box::pin(async move {
            let (sender, receiver) = oneshot::channel();
            let sender = Arc::new(Mutex::new(Some(sender)));
            let query = Arc::new(Mutex::new(UUID::zero()));
            let callback_sender = Arc::clone(&sender);
            let callback_query = Arc::clone(&query);
            let expected = name.to_owned();
            let subscription = self
                .directory
                .subscribe_dir_people_reply(Arc::new(move |event| {
                    if event.query_id() != *lock(&callback_query) {
                        return;
                    }
                    let match_id = event
                        .matched_people()
                        .into_iter()
                        .find(|person| person.to_string().eq_ignore_ascii_case(&expected))
                        .map(|person| person.agent_id);
                    if let Some(sender) = lock(&callback_sender).take() {
                        let _ = sender.send(match_id);
                    }
                }));
            let query_id = self
                .directory
                .start_people_search(name.into(), 0)
                .map_err(|_| "start avatar directory search")?;
            *lock(&query) = query_id;
            let result = tokio::select! {
                () = cancellation.cancelled() => Err("avatar lookup cancelled"),
                () = tokio::time::sleep(Duration::from_mins(1)) => Err("avatar lookup timed out"),
                result = receiver => Ok(result.unwrap_or(None)),
            };
            drop(subscription);
            result
        })
    }

    fn accept_teleport(&self, source: UUID, session: UUID) -> Result<(), &'static str> {
        self.with_agent(|agent| agent.teleport_lure_respond(source, session, true))
            .map_err(|_| "accept teleport lure")
    }

    fn pause(&self) -> Result<(), &'static str> {
        self.network
            .current_sim()
            .ok_or("no current simulator")?
            .pause()
            .map_err(|_| "pause simulator")
    }

    fn resume(&self) -> Result<(), &'static str> {
        self.network
            .current_sim()
            .ok_or("no current simulator")?
            .resume()
            .map_err(|_| "resume simulator")
    }

    fn is_group_member(&self, id: UUID) -> bool {
        self.with_agent(|agent| {
            agent
                .group_chat_sessions
                .read()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .values()
                .any(|members| members.iter().any(|member| member.avatar_key == id))
        })
    }

    fn shutdown(&self) {
        if self.closed.swap(true, Ordering::AcqRel) {
            return;
        }
        lock(&self.subscriptions).clear();
        let _ = self.network.logout_with_method();
        let mut client = lock(&self.client);
        let _ = client.self_().dispose();
        let _ = client.dispose_with_method();
    }
}

struct FakeBackend {
    id: UUID,
    name: String,
    connected: AtomicBool,
    calls: Mutex<Vec<String>>,
    people: Arc<Mutex<HashMap<String, UUID>>>,
    group_members: Arc<Mutex<HashSet<UUID>>>,
    inventory_state: Mutex<inventory::State>,
    world_state: Mutex<world::State>,
    services_state: Mutex<services::State>,
}

impl FakeBackend {
    fn new(
        id: UUID,
        name: String,
        people: Arc<Mutex<HashMap<String, UUID>>>,
        group_members: Arc<Mutex<HashSet<UUID>>>,
        policy: inventory::MutationPolicy,
        get_textures: bool,
    ) -> Self {
        Self {
            id,
            name,
            connected: AtomicBool::new(true),
            calls: Mutex::new(Vec::new()),
            people,
            group_members,
            inventory_state: Mutex::new(inventory::State::new(policy)),
            world_state: Mutex::new(world::State::new(policy, get_textures)),
            services_state: Mutex::new(services::State::new(policy)),
        }
    }

    fn record(&self, value: String) {
        lock(&self.calls).push(value);
    }
}

impl ClientBackend for FakeBackend {
    fn id(&self) -> UUID {
        self.id
    }
    fn name(&self) -> String {
        self.name.clone()
    }
    fn connected(&self) -> bool {
        self.connected.load(Ordering::Acquire)
    }
    fn chat(&self, message: &str, channel: i32, kind: ChatType) -> Result<(), &'static str> {
        self.record(format!(
            "CALL chat channel={channel} type={kind:?} {message}"
        ));
        Ok(())
    }
    fn instant_message(&self, target: UUID, message: &str) -> Result<(), &'static str> {
        self.record(format!("CALL instant-message {target} {message}"));
        Ok(())
    }
    fn group_message<'a>(
        &'a self,
        group: UUID,
        message: &'a str,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<(), &'static str>> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err("group-chat join cancelled");
            }
            self.record(format!("CALL group-chat-join {group}"));
            self.record(format!("CALL group-instant-message {group} {message}"));
            Ok(())
        })
    }
    fn resolve_avatar<'a>(
        &'a self,
        name: &'a str,
        cancellation: CancellationToken,
    ) -> BackendFuture<'a, Result<Option<UUID>, &'static str>> {
        let result = lock(&self.people).get(&name.to_ascii_lowercase()).copied();
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                Err("avatar lookup cancelled")
            } else {
                Ok(result)
            }
        })
    }
    fn accept_teleport(&self, source: UUID, session: UUID) -> Result<(), &'static str> {
        self.record(format!(
            "CALL teleport-lure-respond {source} {session} accept=true"
        ));
        Ok(())
    }
    fn pause(&self) -> Result<(), &'static str> {
        self.record("CALL agent-pause".into());
        Ok(())
    }
    fn resume(&self) -> Result<(), &'static str> {
        self.record("CALL agent-resume".into());
        Ok(())
    }
    fn is_group_member(&self, id: UUID) -> bool {
        lock(&self.group_members).contains(&id)
    }
    fn shutdown(&self) {
        if self.connected.swap(false, Ordering::AcqRel) {
            self.record("CALL logout-dispose".into());
        }
    }
    fn take_calls(&self) -> Vec<String> {
        std::mem::take(&mut *lock(&self.calls))
    }
}

enum OutputKind {
    Console,
    Buffered(Mutex<Vec<String>>),
}

struct ProgramOutput(OutputKind);

impl ProgramOutput {
    fn console() -> Arc<Self> {
        Arc::new(Self(OutputKind::Console))
    }

    fn buffered() -> Arc<Self> {
        Arc::new(Self(OutputKind::Buffered(Mutex::new(Vec::new()))))
    }

    fn line(&self, value: impl AsRef<str>) {
        let line = redact_text(value.as_ref());
        match &self.0 {
            OutputKind::Console => println!("{line}"),
            OutputKind::Buffered(lines) => lock(lines).push(line),
        }
    }

    fn drain(&self) -> Vec<String> {
        match &self.0 {
            OutputKind::Console => Vec::new(),
            OutputKind::Buffered(lines) => std::mem::take(&mut *lock(lines)),
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
enum CommandCategory {
    Appearance,
    Communication,
    Friends,
    Groups,
    Inventory,
    Movement,
    Objects,
    Parcel,
    Search,
    Simulator,
    TestClient,
    Other,
}

#[derive(Clone)]
enum CommandHandler {
    At,
    Debug,
    EchoMaster,
    Help,
    Im,
    ImGroup,
    Inventory(inventory::Command),
    Services(services::Command),
    Voice(voice::Command),
    World(world::Command),
    Load,
    Login,
    LogPacket,
    Logout,
    Md5,
    Quit,
    Say(ChatType),
    SetMaster,
    SetMasterKey,
    ShowEffects,
    Sleep,
    WaitForLogin,
    LoadedAlias(String),
}

#[derive(Clone)]
struct CommandDefinition {
    name: String,
    description: String,
    category: CommandCategory,
    handler: CommandHandler,
}

type CommandEntry = (&'static str, &'static str, CommandCategory, CommandHandler);

fn framework_commands() -> [CommandEntry; 14] {
    [
        (
            "@",
            "Restrict following commands to one or all avatars. Usage: @ [firstname lastname]",
            CommandCategory::TestClient,
            CommandHandler::At,
        ),
        (
            "debug",
            "Turn logging on or off. Usage: debug [None|Trace|Debug|Info|Warn|Error|Critical]",
            CommandCategory::TestClient,
            CommandHandler::Debug,
        ),
        (
            "help",
            "Lists available commands. Usage: help [command]",
            CommandCategory::TestClient,
            CommandHandler::Help,
        ),
        (
            "load",
            "Load a bounded native command-alias manifest. Usage: load [file]",
            CommandCategory::TestClient,
            CommandHandler::Load,
        ),
        (
            "login",
            "Log in another avatar. Usage: login firstname lastname password [simname] [loginuri]",
            CommandCategory::TestClient,
            CommandHandler::Login,
        ),
        (
            "logpacket",
            "Log packet metadata. Usage: logpacket [count] [file]",
            CommandCategory::TestClient,
            CommandHandler::LogPacket,
        ),
        (
            "logout",
            "Log this avatar out",
            CommandCategory::TestClient,
            CommandHandler::Logout,
        ),
        (
            "md5",
            "Create the LibreMetaverse login MD5 hash. Usage: md5 [password]",
            CommandCategory::Other,
            CommandHandler::Md5,
        ),
        (
            "quit",
            "Log all avatars out and shut down",
            CommandCategory::TestClient,
            CommandHandler::Quit,
        ),
        (
            "setmaster",
            "Resolve and set the master. Usage: setmaster [name]",
            CommandCategory::TestClient,
            CommandHandler::SetMaster,
        ),
        (
            "setmasterkey",
            "Set the master UUID. Usage: setmasterkey [uuid]",
            CommandCategory::TestClient,
            CommandHandler::SetMasterKey,
        ),
        (
            "showeffects",
            "Print viewer effects. Usage: showeffects [on|off]",
            CommandCategory::Other,
            CommandHandler::ShowEffects,
        ),
        (
            "sleep",
            "Pause, sleep, and resume. Usage: sleep [seconds]",
            CommandCategory::TestClient,
            CommandHandler::Sleep,
        ),
        (
            "waitforlogin",
            "Wait for all current login attempts",
            CommandCategory::TestClient,
            CommandHandler::WaitForLogin,
        ),
    ]
}

fn communication_commands() -> [CommandEntry; 6] {
    [
        (
            "echomaster",
            "Repeat fully audible normal chat from the configured master",
            CommandCategory::Communication,
            CommandHandler::EchoMaster,
        ),
        (
            "im",
            "Instant message someone. Usage: im [firstname] [lastname] [message] | im [avatar-uuid] [message]",
            CommandCategory::Communication,
            CommandHandler::Im,
        ),
        (
            "imgroup",
            "Send a group instant message. Usage: imgroup [group_uuid] [message]",
            CommandCategory::Communication,
            CommandHandler::ImGroup,
        ),
        (
            "say",
            "Say something. Usage: say [optional-channel] message",
            CommandCategory::Communication,
            CommandHandler::Say(ChatType::Normal),
        ),
        (
            "shout",
            "Shout something. Usage: shout [optional-channel] message",
            CommandCategory::Communication,
            CommandHandler::Say(ChatType::Shout),
        ),
        (
            "whisper",
            "Whisper something. Usage: whisper [optional-channel] message",
            CommandCategory::Communication,
            CommandHandler::Say(ChatType::Whisper),
        ),
    ]
}

fn built_in_commands() -> HashMap<String, CommandDefinition> {
    framework_commands()
        .into_iter()
        .chain(communication_commands())
        .chain(inventory::commands())
        .chain(services::commands())
        .chain(voice::commands())
        .chain(world::commands())
        .map(|(name, description, category, handler)| {
            (
                name.into(),
                CommandDefinition {
                    name: name.into(),
                    description: description.into(),
                    category,
                    handler,
                },
            )
        })
        .collect()
}

struct PacketLog {
    writer: BufWriter<File>,
    remaining: usize,
    written: u64,
}

impl PacketLog {
    fn create(path: &Path, count: usize) -> Result<Self, io::Error> {
        Ok(Self {
            writer: BufWriter::new(File::create(path)?),
            remaining: count,
            written: 0,
        })
    }

    fn record(&mut self, packet_type: &str, simulator: &str, bytes: usize) -> io::Result<bool> {
        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis();
        let line = redact_text(&format!(
            "Received unix-ms={timestamp} type={} simulator={} bytes={bytes}",
            clean_text(packet_type, 128),
            clean_text(simulator, 256)
        ));
        let length = u64::try_from(line.len())
            .unwrap_or(u64::MAX)
            .saturating_add(1);
        if self.written.saturating_add(length) > MAX_PACKET_LOG_BYTES {
            self.writer.flush()?;
            return Ok(true);
        }
        writeln!(self.writer, "{line}")?;
        self.written += length;
        self.remaining = self.remaining.saturating_sub(1);
        if self.remaining == 0 {
            self.writer.flush()?;
            Ok(true)
        } else {
            Ok(false)
        }
    }
}

#[derive(Clone, Copy)]
struct ClientOptions(u8);

impl ClientOptions {
    const ALLOW_OBJECT_MASTER: u8 = 1;
    const GROUP_COMMANDS: u8 = 1 << 1;
    const ECHO_MASTER: u8 = 1 << 2;
    const SHOW_EFFECTS: u8 = 1 << 3;

    fn new(master_key: UUID, group_commands: bool) -> Self {
        let mut bits = 0;
        if master_key != UUID::zero() {
            bits |= Self::ALLOW_OBJECT_MASTER;
        }
        if group_commands {
            bits |= Self::GROUP_COMMANDS;
        }
        Self(bits)
    }

    const fn contains(self, option: u8) -> bool {
        self.0 & option != 0
    }

    const fn allow_object_master(self) -> bool {
        self.contains(Self::ALLOW_OBJECT_MASTER)
    }

    const fn group_commands(self) -> bool {
        self.contains(Self::GROUP_COMMANDS)
    }

    const fn echo_master(self) -> bool {
        self.contains(Self::ECHO_MASTER)
    }

    const fn show_effects(self) -> bool {
        self.contains(Self::SHOW_EFFECTS)
    }

    fn set(&mut self, option: u8, enabled: bool) {
        if enabled {
            self.0 |= option;
        } else {
            self.0 &= !option;
        }
    }

    fn toggle_echo_master(&mut self) -> bool {
        self.0 ^= Self::ECHO_MASTER;
        self.echo_master()
    }
}

struct ManagedClient {
    backend: Arc<dyn ClientBackend>,
    master_name: String,
    master_key: UUID,
    options: ClientOptions,
    packet_log: Option<PacketLog>,
}

impl ManagedClient {
    fn name(&self) -> String {
        self.backend.name()
    }
}

#[derive(Clone, Copy, Eq, PartialEq)]
enum RunMode {
    Live,
    Fake,
}

struct ClientManager {
    clients: HashMap<UUID, ManagedClient>,
    selected: Option<String>,
    commands: HashMap<String, CommandDefinition>,
    running: bool,
    pending_logins: usize,
    mode: RunMode,
    login_uri: Option<String>,
    login_timeout: Duration,
    default_master_name: String,
    default_master_key: UUID,
    default_group_commands: bool,
    default_mutation_policy: inventory::MutationPolicy,
    default_get_textures: bool,
    cancellation: CancellationTokenSource,
    output: Arc<ProgramOutput>,
    events_tx: mpsc::Sender<ClientEvent>,
    events_rx: mpsc::Receiver<ClientEvent>,
    dropped_events: Arc<AtomicUsize>,
    fake_people: Arc<Mutex<HashMap<String, UUID>>>,
    fake_group_members: Arc<Mutex<HashSet<UUID>>>,
    archived_calls: Vec<String>,
    fake_id: AtomicU64,
    debug_level: String,
}

impl ClientManager {
    #[allow(clippy::too_many_arguments)]
    fn new(
        mode: RunMode,
        login_uri: Option<String>,
        login_timeout: Duration,
        master_name: String,
        master_key: UUID,
        group_commands: bool,
        mutation_policy: inventory::MutationPolicy,
        get_textures: bool,
        output: Arc<ProgramOutput>,
    ) -> Self {
        let (events_tx, events_rx) = mpsc::channel(EVENT_QUEUE_CAPACITY);
        Self {
            clients: HashMap::new(),
            selected: None,
            commands: built_in_commands(),
            running: true,
            pending_logins: 0,
            mode,
            login_uri,
            login_timeout,
            default_master_name: master_name,
            default_master_key: master_key,
            default_group_commands: group_commands,
            default_mutation_policy: mutation_policy,
            default_get_textures: get_textures,
            cancellation: CancellationTokenSource::new(),
            output,
            events_tx,
            events_rx,
            dropped_events: Arc::new(AtomicUsize::new(0)),
            fake_people: Arc::new(Mutex::new(HashMap::new())),
            fake_group_members: Arc::new(Mutex::new(HashSet::new())),
            archived_calls: Vec::new(),
            fake_id: AtomicU64::new(1),
            debug_level: "Debug".into(),
        }
    }

    fn target_ids(&self) -> Vec<UUID> {
        let mut clients: Vec<_> = self
            .clients
            .iter()
            .filter(|(_, client)| {
                self.selected
                    .as_ref()
                    .is_none_or(|selected| client.name() == *selected)
            })
            .map(|(id, client)| (*id, client.name()))
            .collect();
        clients.sort_by(|left, right| left.1.cmp(&right.1));
        clients.into_iter().map(|(id, _)| id).collect()
    }

    fn add_client(&mut self, client: ManagedClient) {
        let name = client.name();
        let duplicates: Vec<_> = self
            .clients
            .iter()
            .filter(|(_, existing)| existing.name().eq_ignore_ascii_case(&name))
            .map(|(id, _)| *id)
            .collect();
        for id in duplicates {
            if let Some(existing) = self.clients.remove(&id) {
                self.archive_client(&existing);
            }
        }
        let id = client.backend.id();
        self.clients.insert(id, client);
        self.output.line(format!("Logged in {name} ({id})"));
    }

    fn add_fake_client(
        &mut self,
        id: UUID,
        name: String,
        master_name: String,
        master_key: UUID,
        group_commands: bool,
    ) {
        let backend = Arc::new(FakeBackend::new(
            id,
            name,
            Arc::clone(&self.fake_people),
            Arc::clone(&self.fake_group_members),
            self.default_mutation_policy,
            self.default_get_textures,
        ));
        self.add_client(ManagedClient {
            backend,
            master_name,
            master_key,
            options: ClientOptions::new(master_key, group_commands),
            packet_log: None,
        });
    }

    async fn login_account(&mut self, account: Account) {
        if self.clients.len() >= MAX_CLIENTS {
            self.output.line("Login rejected: client limit reached");
            return;
        }
        self.pending_logins += 1;
        let display_name = format!("{} {}", account.first, account.last);
        let master_name = account.master_name.clone();
        let configured_master_key = account.master_key;
        let group_commands = account.group_commands;
        let result: Result<Arc<dyn ClientBackend>, ProgramError> = match self.mode {
            RunMode::Live => LiveBackend::login(
                account,
                self.login_timeout,
                self.events_tx.clone(),
                Arc::clone(&self.dropped_events),
                self.default_mutation_policy,
                self.default_get_textures,
                self.cancellation.token(),
            )
            .await
            .map(|backend| backend as Arc<dyn ClientBackend>),
            RunMode::Fake => {
                let sequence = self.fake_id.fetch_add(1, Ordering::AcqRel);
                let text = format!("00000000-0000-0000-0000-{sequence:012x}");
                match UUID::new_with_string(text) {
                    Ok(id) => Ok(Arc::new(FakeBackend::new(
                        id,
                        display_name,
                        Arc::clone(&self.fake_people),
                        Arc::clone(&self.fake_group_members),
                        self.default_mutation_policy,
                        self.default_get_textures,
                    ))),
                    Err(_) => Err(ProgramError::Client(
                        "could not make fake client UUID".into(),
                    )),
                }
            }
        };
        self.pending_logins = self.pending_logins.saturating_sub(1);
        match result {
            Ok(backend) => {
                let mut master_key = configured_master_key;
                if master_key == UUID::zero() && !master_name.is_empty() {
                    match backend
                        .resolve_avatar(&master_name, self.cancellation.token())
                        .await
                    {
                        Ok(Some(id)) => master_key = id,
                        Ok(None) | Err(_) => self
                            .output
                            .line(format!("Unable to resolve master key from {master_name}")),
                    }
                }
                self.add_client(ManagedClient {
                    backend,
                    master_name,
                    master_key,
                    options: ClientOptions::new(master_key, group_commands),
                    packet_log: None,
                });
            }
            Err(error) => self.output.line(format!("Login failed: {error}")),
        }
    }

    async fn dispatch_line(&mut self, command_line: &str, from: UUID) {
        if command_line.len() > MAX_COMMAND_BYTES {
            self.output
                .line("Command rejected: line exceeds 4,096 bytes");
            return;
        }
        let mut expanded = command_line.trim().to_owned();
        for depth in 0..=MAX_ALIAS_DEPTH {
            let tokens = match tokenize(&expanded) {
                Ok(tokens) => tokens,
                Err(error) => {
                    self.output.line(format!("Command parse error: {error}"));
                    return;
                }
            };
            if tokens.is_empty() || tokens[0].starts_with(';') || tokens[0].starts_with('#') {
                return;
            }
            let name = tokens[0].to_ascii_lowercase();
            let args = &tokens[1..];
            if name == "@" || name.starts_with('@') {
                self.select_avatar(args);
                return;
            }
            let Some(command) = self.commands.get(&name).cloned() else {
                self.output.line(format!("Unknown command {name}"));
                return;
            };
            if let CommandHandler::LoadedAlias(template) = &command.handler {
                if depth == MAX_ALIAS_DEPTH {
                    self.output
                        .line("Loaded command alias recursion limit reached");
                    return;
                }
                let args = args.join(" ");
                expanded = template.replace("{args}", &args).replace("$*", &args);
                continue;
            }
            self.dispatch_definition(command, args, from).await;
            return;
        }
    }

    fn select_avatar(&mut self, args: &[String]) {
        if args.is_empty() {
            self.selected = None;
            self.output.line("Commanding all avatars now");
            return;
        }
        if args.len() != 2 {
            self.output.line("Usage: @ [firstname lastname]");
            return;
        }
        let name = format!("{} {}", args[0], args[1]);
        let found = self
            .clients
            .values()
            .any(|client| client.name() == name && client.backend.connected());
        self.selected = Some(name.clone());
        if found {
            self.output.line(format!("Commanding only {name} now"));
        } else {
            self.output
                .line(format!("Commanding nobody now. Avatar {name} is offline"));
        }
    }

    async fn dispatch_definition(
        &mut self,
        command: CommandDefinition,
        args: &[String],
        from: UUID,
    ) {
        match command.handler {
            CommandHandler::Login => {
                match self.account_from_login_args(args) {
                    Ok(account) => self.login_account(account).await,
                    Err(message) => self.output.line(message),
                }
                return;
            }
            CommandHandler::Quit => {
                self.running = false;
                self.cancellation.cancel();
                self.shutdown_all();
                self.output
                    .line("All clients logged out and program finished running.");
                return;
            }
            CommandHandler::Help => {
                self.show_help(args);
                return;
            }
            CommandHandler::Load => {
                let result = self.load_commands(args);
                self.output.line(result);
                return;
            }
            CommandHandler::WaitForLogin => {
                self.output.line(format!(
                    "All pending logins have completed, currently tracking {} bots",
                    self.clients.len()
                ));
                return;
            }
            CommandHandler::Debug => {
                let result = self.set_debug(args);
                self.output.line(result);
                return;
            }
            CommandHandler::Inventory(inventory::Command::Script) => {
                let result = self.execute_script(args).await;
                self.output.line(result);
                return;
            }
            CommandHandler::At | CommandHandler::LoadedAlias(_) => return,
            _ => {}
        }

        let targets = self.target_ids();
        if targets.is_empty() {
            self.output.line("No matching online avatars");
            return;
        }
        if matches!(command.handler, CommandHandler::Logout) {
            for id in targets {
                if let Some(client) = self.clients.remove(&id) {
                    let name = client.name();
                    self.archive_client(&client);
                    self.output.line(format!("Logged {name} out"));
                }
            }
            return;
        }

        for id in targets {
            let Some(client) = self.clients.get_mut(&id) else {
                continue;
            };
            let result = execute_client_command(
                client,
                &command.handler,
                args,
                from,
                self.cancellation.token(),
            )
            .await;
            self.output.line(format!("[{}] {result}", client.name()));
        }
    }

    fn account_from_login_args(&self, args: &[String]) -> Result<Account, String> {
        if args.len() < 3 {
            return Err(
                "Usage: login firstname lastname password [simname] [login server url]".into(),
            );
        }
        let mut start = None;
        let mut uri = self.login_uri.clone();
        if let Some(value) = args.get(3) {
            if value.starts_with("http://") || value.starts_with("https://") {
                uri = Some(value.clone());
            } else {
                start = Some(parse_start_location(value)?);
            }
        }
        if let Some(value) = args.get(4)
            && (value.starts_with("http://") || value.starts_with("https://"))
        {
            uri = Some(value.clone());
        }
        Ok(Account {
            first: args[0].clone(),
            last: args[1].clone(),
            password: args[2].clone(),
            start,
            uri,
            master_name: self.default_master_name.clone(),
            master_key: self.default_master_key,
            group_commands: self.default_group_commands,
        })
    }

    fn show_help(&self, args: &[String]) {
        if let Some(name) = args.first() {
            let name = name.to_ascii_lowercase();
            self.output.line(self.commands.get(&name).map_or_else(
                || format!("Command {name} does not exist. 'help' displays all commands."),
                |command| command.description.clone(),
            ));
            return;
        }
        let mut commands: Vec<_> = self.commands.values().cloned().collect();
        commands.sort_by(|left, right| {
            left.category
                .cmp(&right.category)
                .then_with(|| left.name.cmp(&right.name))
        });
        let mut current = None;
        for command in commands {
            if current != Some(command.category) {
                current = Some(command.category);
                self.output
                    .line(format!("* {:?} Related Commands:", command.category));
            }
            self.output
                .line(format!("  {:15} {}", command.name, command.description));
        }
        self.output.line("Help [command] for usage/information");
    }

    fn set_debug(&mut self, args: &[String]) -> String {
        if args.len() != 1 {
            return "Usage: debug [level] where level is one of None, Trace, Debug, Info, Warn, Error, Critical".into();
        }
        let level = match args[0].to_ascii_lowercase().as_str() {
            "none" => "None",
            "trace" => "Trace",
            "debug" => "Debug",
            "info" => "Info",
            "warn" | "warning" => "Warning",
            "error" => "Error",
            "critical" => "Critical",
            _ => return "Usage: debug [level] where level is one of None, Trace, Debug, Info, Warn, Error, Critical".into(),
        };
        self.debug_level = level.into();
        format!("Logging is set to {level}")
    }

    async fn execute_script(&mut self, args: &[String]) -> String {
        if args.len() != 1 {
            return "Usage: script [filename]".into();
        }
        let lines = match read_bounded_lines(Path::new(&args[0])) {
            Ok(lines) => lines,
            Err(error) => return error.to_string(),
        };
        let count = lines.len();
        for line in lines {
            if !self.running {
                break;
            }
            Box::pin(self.dispatch_line(line.trim(), UUID::zero())).await;
            drain_events(self).await;
        }
        format!("Finished executing {count} commands")
    }

    fn load_commands(&mut self, args: &[String]) -> String {
        if args.len() != 1 {
            return "Usage: load [native-command-manifest]".into();
        }
        let path = Path::new(&args[0]);
        let lines = match read_bounded_lines(path) {
            Ok(lines) => lines,
            Err(error) => return format!("Could not load {}: {error}", path.display()),
        };
        let mut loaded = Vec::new();
        for (index, line) in lines.into_iter().enumerate() {
            if line.trim().is_empty() || line.trim_start().starts_with('#') {
                continue;
            }
            if loaded.len() == MAX_LOADED_COMMANDS {
                return "Command manifest exceeds 128 aliases".into();
            }
            let fields: Vec<_> = line.split('\t').collect();
            let [name, description, template] = fields.as_slice() else {
                return format!("Invalid command manifest line {}", index + 1);
            };
            let name = name.to_ascii_lowercase();
            if !valid_command_name(&name)
                || description.is_empty()
                || template.is_empty()
                || self.commands.contains_key(&name)
                || loaded
                    .iter()
                    .any(|command: &CommandDefinition| command.name == name)
            {
                return format!("Invalid or duplicate command manifest line {}", index + 1);
            }
            loaded.push(CommandDefinition {
                name,
                description: clean_text(description, 512),
                category: CommandCategory::Other,
                handler: CommandHandler::LoadedAlias(clean_text(template, MAX_COMMAND_BYTES)),
            });
        }
        let count = loaded.len();
        for command in loaded {
            self.commands.insert(command.name.clone(), command);
        }
        format!(
            "Loaded {count} native command alias(es) from {}",
            path.display()
        )
    }

    fn process_event(&mut self, event: ClientEvent) -> Option<(String, UUID)> {
        let client_id = match &event {
            ClientEvent::Chat { client, .. }
            | ClientEvent::InstantMessage { client, .. }
            | ClientEvent::Packet { client, .. }
            | ClientEvent::Effect { client, .. }
            | ClientEvent::Disconnected { client, .. } => *client,
        };
        let client = self.clients.get_mut(&client_id)?;
        match event {
            ClientEvent::Chat {
                source,
                name,
                message,
                normal,
                fully_audible,
                ..
            } => {
                let authorized = source == client.master_key
                    || (!client.options.allow_object_master() && name == client.master_name);
                if client.options.echo_master()
                    && authorized
                    && normal
                    && fully_audible
                    && !message.is_empty()
                    && let Err(error) = client.backend.chat(&message, 0, ChatType::Normal)
                {
                    self.output.line(format!("Echo failed: {error}"));
                }
            }
            ClientEvent::InstantMessage {
                source,
                name,
                message,
                dialog,
                session,
                group,
                ..
            } => {
                let authorized = source == client.master_key
                    || (client.options.group_commands()
                        && group
                        && client.backend.is_group_member(source));
                self.output.line(format!(
                    "<{} ({dialog:?})> {}{}: {}",
                    if group { "GroupIM" } else { "IM" },
                    clean_text(&name, 256),
                    if authorized { "" } else { " (not master)" },
                    sanitize_remote_message(&message)
                ));
                if !authorized {
                    return None;
                }
                if dialog == InstantMessageDialog::RequestTeleport {
                    if let Err(error) = client.backend.accept_teleport(source, session) {
                        self.output
                            .line(format!("Teleport response failed: {error}"));
                    }
                } else if matches!(
                    dialog,
                    InstantMessageDialog::MessageFromAgent
                        | InstantMessageDialog::MessageFromObject
                ) {
                    return Some((message, source));
                }
            }
            ClientEvent::Packet {
                packet_type,
                simulator,
                bytes,
                ..
            } => {
                if let Some(log) = &mut client.packet_log {
                    match log.record(&packet_type, &simulator, bytes) {
                        Ok(true) => {
                            client.packet_log = None;
                            self.output
                                .line(format!("Finished logging packets for {}", client.name()));
                        }
                        Ok(false) => {}
                        Err(error) => {
                            client.packet_log = None;
                            self.output.line(format!("Packet logging failed: {error}"));
                        }
                    }
                }
            }
            ClientEvent::Effect { summary, .. } => {
                if client.options.show_effects() {
                    self.output.line(summary);
                }
            }
            ClientEvent::Disconnected { reason, .. } => {
                self.output
                    .line(format!("{} disconnected: {reason}", client.name()));
            }
        }
        None
    }

    fn shutdown_all(&mut self) {
        for client in self.clients.values() {
            client.backend.shutdown();
        }
        for client in self.clients.values_mut() {
            if let Some(mut log) = client.packet_log.take() {
                let _ = log.writer.flush();
            }
        }
    }

    fn archive_client(&mut self, client: &ManagedClient) {
        let name = client.name();
        client.backend.shutdown();
        self.archived_calls.extend(
            client
                .backend
                .take_calls()
                .into_iter()
                .map(|call| format!("[{name}] {call}")),
        );
    }

    fn take_fake_calls(&self) -> Vec<String> {
        let mut clients: Vec<_> = self.clients.values().collect();
        clients.sort_by_key(|client| client.name());
        let mut calls = self.archived_calls.clone();
        for client in clients {
            let name = client.name();
            calls.extend(
                client
                    .backend
                    .take_calls()
                    .into_iter()
                    .map(|call| format!("[{name}] {call}")),
            );
        }
        calls
    }
}

async fn execute_client_command(
    client: &mut ManagedClient,
    handler: &CommandHandler,
    args: &[String],
    from: UUID,
    cancellation: CancellationToken,
) -> String {
    match handler {
        CommandHandler::EchoMaster
        | CommandHandler::Im
        | CommandHandler::ImGroup
        | CommandHandler::Say(_) => {
            execute_communication_command(client, handler, args, cancellation).await
        }
        CommandHandler::LogPacket => configure_packet_log(client, args),
        CommandHandler::Inventory(command) => {
            inventory::execute(client.backend.as_ref(), *command, args, from, cancellation).await
        }
        CommandHandler::Services(command) => {
            services::execute(client.backend.as_ref(), *command, args, cancellation).await
        }
        CommandHandler::Voice(command) => {
            voice::execute(client.backend.as_ref(), *command, args, cancellation).await
        }
        CommandHandler::World(command) => {
            world::execute(client.backend.as_ref(), *command, args, cancellation).await
        }
        CommandHandler::Md5 => {
            if args.len() != 1 {
                return "Usage: md5 [password]".into();
            }
            Utils::md5_with_string(args[0].clone())
                .unwrap_or_else(|_| "Unable to create MD5 hash".into())
        }
        CommandHandler::SetMaster | CommandHandler::SetMasterKey | CommandHandler::ShowEffects => {
            configure_client(client, handler, args, cancellation).await
        }
        CommandHandler::Sleep => sleep_client(client, args, cancellation).await,
        CommandHandler::At
        | CommandHandler::Debug
        | CommandHandler::Help
        | CommandHandler::Load
        | CommandHandler::Login
        | CommandHandler::Logout
        | CommandHandler::Quit
        | CommandHandler::WaitForLogin
        | CommandHandler::LoadedAlias(_) => "Command is handled by the client manager".into(),
    }
}

async fn execute_communication_command(
    client: &mut ManagedClient,
    handler: &CommandHandler,
    args: &[String],
    cancellation: CancellationToken,
) -> String {
    match handler {
        CommandHandler::EchoMaster => {
            if client.options.toggle_echo_master() {
                "Echoing is now on.".into()
            } else {
                "Echoing is now off.".into()
            }
        }
        CommandHandler::Im => {
            if args.is_empty() {
                return "Usage: im [firstname] [lastname] [message] | im [avatar-uuid] [message]"
                    .into();
            }
            let (target, message) = if let Ok(target) = UUID::new_with_string(args[0].clone()) {
                if args.len() < 2 {
                    return "Usage: im [firstname] [lastname] [message] | im [avatar-uuid] [message]"
                        .into();
                }
                (target, bounded_message(&args[1..].join(" ")))
            } else {
                if args.len() < 3 {
                    return "Usage: im [firstname] [lastname] [message] | im [avatar-uuid] [message]"
                        .into();
                }
                let name = format!("{} {}", args[0], args[1]);
                let target = match client.backend.resolve_avatar(&name, cancellation).await {
                    Ok(Some(target)) => target,
                    Ok(None) => return format!("Name lookup for {name} failed"),
                    Err(error) => return format!("Name lookup for {name} failed: {error}"),
                };
                (target, bounded_message(&args[2..].join(" ")))
            };
            match client.backend.instant_message(target, &message) {
                Ok(()) => format!("Instant Messaged {target} with message: {message}"),
                Err(error) => format!("Instant message failed: {error}"),
            }
        }
        CommandHandler::ImGroup => {
            if args.len() < 2 {
                return "Usage: imgroup [group_uuid] [message]".into();
            }
            let Ok(group) = UUID::new_with_string(args[0].clone()) else {
                return "failed to instant message group".into();
            };
            let message = bounded_message(&args[1..].join(" "));
            match client
                .backend
                .group_message(group, &message, cancellation)
                .await
            {
                Ok(()) => format!("Instant Messaged group {group} with message: {message}"),
                Err(error) => format!("Group instant message failed: {error}"),
            }
        }
        CommandHandler::Say(kind) => {
            if args.is_empty() {
                return format!("usage: {} (optional channel) whatever", chat_verb(*kind));
            }
            let (channel, start) = if args.len() > 1 {
                args[0]
                    .parse::<i32>()
                    .map_or((0, 0), |channel| (channel, 1))
            } else {
                (0, 0)
            };
            let message = bounded_message(&args[start..].join(" "));
            match client.backend.chat(&message, channel, *kind) {
                Ok(()) => format!("{} {message}", chat_past_tense(*kind)),
                Err(error) => format!("Chat failed: {error}"),
            }
        }
        _ => unreachable!("manager only routes communication commands here"),
    }
}

fn configure_packet_log(client: &mut ManagedClient, args: &[String]) -> String {
    if args.len() != 2 {
        return "Usage: logpacket no-of-packets filename".into();
    }
    let Ok(count) = args[0].parse::<usize>() else {
        return format!("{} is not a valid number of packets", args[0]);
    };
    if count == 0 || count > MAX_PACKET_LOG_COUNT {
        return format!(
            "{} is not a valid number of packets (allowed 1..={MAX_PACKET_LOG_COUNT})",
            args[0]
        );
    }
    if let Some(log) = &client.packet_log {
        return format!(
            "Still waiting to finish logging {} packets for {}",
            log.remaining,
            client.name()
        );
    }
    match PacketLog::create(Path::new(&args[1]), count) {
        Ok(log) => {
            client.packet_log = Some(log);
            format!("Now logging {count} packets for {}", client.name())
        }
        Err(error) => format!("Could not open packet log: {error}"),
    }
}

async fn configure_client(
    client: &mut ManagedClient,
    handler: &CommandHandler,
    args: &[String],
    cancellation: CancellationToken,
) -> String {
    match handler {
        CommandHandler::SetMaster => {
            let name = args.join(" ").trim().to_owned();
            if name.is_empty() {
                return "Usage: setmaster [name]".into();
            }
            let Ok(Some(id)) = client.backend.resolve_avatar(&name, cancellation).await else {
                return format!("Unable to obtain UUID for \"{name}\". Master unchanged.");
            };
            client.master_name.clone_from(&name);
            client.master_key = id;
            client.options.set(ClientOptions::ALLOW_OBJECT_MASTER, true);
            let _ = client.backend.instant_message(
                id,
                "You are now my master. IM me with \"help\" for a command list.",
            );
            format!("Master set to {name} ({id})")
        }
        CommandHandler::SetMasterKey => {
            if args.len() != 1 {
                return "Usage: setmasterkey [uuid]".into();
            }
            let Ok(id) = UUID::new_with_string(args[0].clone()) else {
                return "Master UUID is invalid".into();
            };
            client.master_key = id;
            client
                .options
                .set(ClientOptions::ALLOW_OBJECT_MASTER, id != UUID::zero());
            if id != UUID::zero() {
                let _ = client.backend.instant_message(
                    id,
                    "You are now my master. IM me with \"help\" for a command list.",
                );
            }
            format!("Master set to {id}")
        }
        CommandHandler::ShowEffects => {
            if args.len() > 1 {
                return "Usage: showeffects [on/off]".into();
            }
            let enabled = args.first().is_none_or(|value| value == "on");
            client.options.set(ClientOptions::SHOW_EFFECTS, enabled);
            if enabled {
                "Viewer effects will be shown on the console".into()
            } else {
                "Viewer effects will not be shown".into()
            }
        }
        _ => unreachable!("manager only routes client configuration commands here"),
    }
}

async fn sleep_client(
    client: &ManagedClient,
    args: &[String],
    cancellation: CancellationToken,
) -> String {
    let Ok(seconds) = args.first().map_or("", String::as_str).parse::<u64>() else {
        return "Usage: sleep [seconds]".into();
    };
    if args.len() != 1 || seconds > 3600 {
        return "Usage: sleep [seconds] (maximum 3600)".into();
    }
    if let Err(error) = client.backend.pause() {
        return format!("Pause failed: {error}");
    }
    let cancelled = tokio::select! {
        () = cancellation.cancelled() => true,
        () = tokio::time::sleep(Duration::from_secs(seconds)) => false,
    };
    if let Err(error) = client.backend.resume() {
        return format!("Resume failed: {error}");
    }
    if cancelled {
        "Pause cancelled; agent resumed".into()
    } else {
        format!("Paused, slept for {seconds} second(s), and resumed")
    }
}

const fn chat_verb(kind: ChatType) -> &'static str {
    match kind {
        ChatType::Whisper => "whisper",
        ChatType::Shout => "shout",
        _ => "say",
    }
}

const fn chat_past_tense(kind: ChatType) -> &'static str {
    match kind {
        ChatType::Whisper => "Whispered",
        ChatType::Shout => "Shouted",
        _ => "Said",
    }
}

#[must_use]
pub fn main_entry() -> ExitCode {
    let cli = Cli::parse();
    let Ok(runtime) = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
    else {
        eprintln!("test-client: could not initialize the async runtime");
        return ExitCode::from(EXIT_CLIENT);
    };
    let result = runtime.block_on(run(cli));
    runtime.shutdown_timeout(Duration::from_secs(1));
    match result {
        Ok(()) => ExitCode::from(EXIT_SUCCESS),
        Err(error) => {
            eprintln!("test-client: {}", redact_text(&error.to_string()));
            ExitCode::from(error.exit_code())
        }
    }
}

async fn run(cli: Cli) -> Result<(), ProgramError> {
    let mutation_policy = inventory::MutationPolicy::new(
        cli.allow_live_mutations,
        cli.allow_spending,
        cli.allow_estate_actions,
    );
    let master_key = match cli.masterkey.as_deref() {
        Some(value) => UUID::new_with_string(value.into())
            .map_err(|_| ProgramError::Usage("--masterkey must be a UUID"))?,
        None => UUID::zero(),
    };
    let login_uri = cli
        .loginuri
        .clone()
        .or_else(|| std::env::var("GRID_LOGIN_URL").ok())
        .filter(|value| !value.is_empty());
    let timeout = Duration::from_secs(cli.login_timeout_seconds);
    if let Some(path) = cli.fake_script.as_deref() {
        let output = ProgramOutput::buffered();
        let mut manager = ClientManager::new(
            RunMode::Fake,
            login_uri,
            timeout,
            cli.master.unwrap_or_default(),
            master_key,
            cli.groupcommands,
            mutation_policy,
            cli.gettextures,
            Arc::clone(&output),
        );
        run_fake_script(&mut manager, path).await?;
        manager.shutdown_all();
        let calls = manager.take_fake_calls();
        let stdout = io::stdout();
        let mut stdout = stdout.lock();
        for line in output.drain().into_iter().chain(calls) {
            writeln!(stdout, "{}", redact_text(&line))
                .map_err(|source| input_error("writing standard output", source))?;
        }
        writeln!(
            stdout,
            "STATE clients={} connected=0 pending-logins={} active-tasks=0 shutdown=true dropped-events={} pending-command-targets={}",
            manager.clients.len(),
            manager.pending_logins,
            manager.dropped_events.load(Ordering::Acquire),
            pending_test_client_commands().len(),
        )
        .map_err(|source| input_error("writing standard output", source))?;
        return Ok(());
    }

    let output = ProgramOutput::console();
    let accounts = resolve_initial_accounts(&cli, login_uri.clone(), master_key)?;
    let mut manager = ClientManager::new(
        RunMode::Live,
        login_uri,
        timeout,
        cli.master.unwrap_or_default(),
        master_key,
        cli.groupcommands,
        mutation_policy,
        cli.gettextures,
        Arc::clone(&output),
    );
    for account in accounts {
        manager.login_account(account).await;
    }
    if let Some(path) = cli.scriptfile.as_deref() {
        run_command_file(&mut manager, path).await?;
    }
    if manager.running {
        if cli.nogui {
            run_headless(&mut manager).await?;
        } else {
            run_interactive(&mut manager).await?;
        }
    }
    manager.cancellation.cancel();
    manager.shutdown_all();
    let dropped = manager.dropped_events.load(Ordering::Acquire);
    if dropped > 0 {
        eprintln!("test-client: dropped {dropped} events because the bounded queue was full");
    }
    Ok(())
}

fn resolve_initial_accounts(
    cli: &Cli,
    login_uri: Option<String>,
    master_key: UUID,
) -> Result<Vec<Account>, ProgramError> {
    if cli.file.is_some() && (cli.first.is_some() || cli.last.is_some() || cli.pass.is_some()) {
        return Err(ProgramError::Usage(
            "--file cannot be combined with --first, --last, or --pass",
        ));
    }
    let start = cli
        .startpos
        .as_deref()
        .map(parse_start_location)
        .transpose()
        .map_err(|_| ProgramError::Usage("--startpos must be Region/x/y/z"))?;
    if let Some(path) = cli.file.as_deref() {
        let lines = read_bounded_lines(path)?;
        let mut accounts = Vec::new();
        for (index, line) in lines.into_iter().enumerate() {
            if line.trim().is_empty() || line.trim_start().starts_with('#') {
                continue;
            }
            if accounts.len() == MAX_CLIENTS {
                return Err(ProgramError::InvalidInput {
                    line: index + 1,
                    reason: "account file exceeds 64 clients",
                });
            }
            let values: Vec<_> = line
                .split(|ch: char| ch.is_ascii_whitespace() || ch == ',')
                .filter(|value| !value.is_empty())
                .collect();
            if values.len() < 3 || values.len() > 4 {
                return Err(ProgramError::InvalidInput {
                    line: index + 1,
                    reason: "expected First Last Password [Region/x/y/z]",
                });
            }
            let account_start = values
                .get(3)
                .map(|value| parse_start_location(value))
                .transpose()
                .map_err(|_| ProgramError::InvalidInput {
                    line: index + 1,
                    reason: "invalid Region/x/y/z start location",
                })?
                .or_else(|| start.clone());
            accounts.push(Account {
                first: values[0].into(),
                last: values[1].into(),
                password: values[2].into(),
                start: account_start,
                uri: login_uri.clone(),
                master_name: cli.master.clone().unwrap_or_default(),
                master_key,
                group_commands: cli.groupcommands,
            });
        }
        return Ok(accounts);
    }
    match (&cli.first, &cli.last, &cli.pass) {
        (None, None, None) => Ok(Vec::new()),
        (Some(first), Some(last), Some(password)) if !password.is_empty() => Ok(vec![Account {
            first: first.clone(),
            last: last.clone(),
            password: password.clone(),
            start,
            uri: login_uri,
            master_name: cli.master.clone().unwrap_or_default(),
            master_key,
            group_commands: cli.groupcommands,
        }]),
        _ => Err(ProgramError::Usage(
            "--first, --last, and --pass must be supplied together",
        )),
    }
}

async fn run_command_file(manager: &mut ClientManager, path: &Path) -> Result<(), ProgramError> {
    for line in read_bounded_lines(path)? {
        if !manager.running {
            break;
        }
        manager.dispatch_line(&line, UUID::zero()).await;
        drain_events(manager).await;
    }
    Ok(())
}

async fn run_headless(manager: &mut ClientManager) -> Result<(), ProgramError> {
    while manager.running {
        tokio::select! {
            signal = tokio::signal::ctrl_c() => {
                signal.map_err(|_| ProgramError::Signal)?;
                break;
            },
            event = manager.events_rx.recv() => {
                let Some(event) = event else { break };
                if let Some((line, from)) = manager.process_event(event) {
                    manager.dispatch_line(&line, from).await;
                }
            }
        }
    }
    Ok(())
}

async fn run_interactive(manager: &mut ClientManager) -> Result<(), ProgramError> {
    manager
        .output
        .line("Type quit to exit. Type help for a command list.");
    let mut lines = AsyncBufReader::new(tokio::io::stdin()).lines();
    while manager.running {
        let online = manager
            .clients
            .values()
            .filter(|client| client.backend.connected())
            .count();
        print!("{online} avatars online> ");
        io::stdout()
            .flush()
            .map_err(|source| input_error("flushing prompt", source))?;
        tokio::select! {
            signal = tokio::signal::ctrl_c() => {
                signal.map_err(|_| ProgramError::Signal)?;
                break;
            },
            line = lines.next_line() => {
                match line.map_err(|source| input_error("reading standard input", source))? {
                    Some(line) => manager.dispatch_line(&line, UUID::zero()).await,
                    None => break,
                }
            },
            event = manager.events_rx.recv() => {
                let Some(event) = event else { break };
                if let Some((line, from)) = manager.process_event(event) {
                    manager.dispatch_line(&line, from).await;
                }
            }
        }
    }
    Ok(())
}

async fn drain_events(manager: &mut ClientManager) {
    while let Ok(event) = manager.events_rx.try_recv() {
        if let Some((line, from)) = manager.process_event(event) {
            Box::pin(manager.dispatch_line(&line, from)).await;
        }
    }
}

async fn run_fake_script(manager: &mut ClientManager, path: &Path) -> Result<(), ProgramError> {
    for (index, line) in read_bounded_lines(path)?.into_iter().enumerate() {
        let line_number = index + 1;
        if line.trim().is_empty() || line.trim_start().starts_with('#') {
            continue;
        }
        if !manager.running {
            break;
        }
        if let Some(directive) = line.strip_prefix('!') {
            run_fake_directive(manager, directive, line_number).await?;
        } else {
            let command = line.strip_prefix("> ").unwrap_or(&line);
            manager.dispatch_line(command, UUID::zero()).await;
        }
        drain_events(manager).await;
    }
    Ok(())
}

async fn run_fake_directive(
    manager: &mut ClientManager,
    directive: &str,
    line: usize,
) -> Result<(), ProgramError> {
    let fields: Vec<_> = directive.split('\t').collect();
    if matches!(
        fields.first().copied(),
        Some("client" | "person" | "group-member")
    ) {
        return run_fake_registry_directive(manager, &fields, line);
    }
    run_fake_event_directive(manager, &fields, line).await
}

fn run_fake_registry_directive(
    manager: &mut ClientManager,
    fields: &[&str],
    line: usize,
) -> Result<(), ProgramError> {
    match fields {
        ["client", id, first, last] => {
            let id = parse_uuid(id, line, "client UUID is invalid")?;
            manager.add_fake_client(
                id,
                format!("{first} {last}"),
                manager.default_master_name.clone(),
                manager.default_master_key,
                manager.default_group_commands,
            );
        }
        [
            "client",
            id,
            first,
            last,
            master_name,
            master_key,
            group_commands,
        ] => {
            let id = parse_uuid(id, line, "client UUID is invalid")?;
            let master_key = parse_uuid(master_key, line, "master UUID is invalid")?;
            let group_commands = match *group_commands {
                "true" => true,
                "false" => false,
                _ => return invalid(line, "groupcommands must be true or false"),
            };
            manager.add_fake_client(
                id,
                format!("{first} {last}"),
                (*master_name).into(),
                master_key,
                group_commands,
            );
        }
        ["person", name, id] => {
            let id = parse_uuid(id, line, "person UUID is invalid")?;
            lock(&manager.fake_people).insert(name.to_ascii_lowercase(), id);
        }
        ["group-member", id] => {
            let id = parse_uuid(id, line, "group member UUID is invalid")?;
            lock(&manager.fake_group_members).insert(id);
        }
        _ => return invalid(line, "invalid client registry fake directive"),
    }
    Ok(())
}

async fn run_fake_event_directive(
    manager: &mut ClientManager,
    fields: &[&str],
    line: usize,
) -> Result<(), ProgramError> {
    if let Some(result) = inventory::apply_fake_directive(manager, fields) {
        result.map_err(|reason| ProgramError::InvalidInput { line, reason })?;
        return Ok(());
    }
    if let Some(result) = world::apply_fake_directive(manager, fields) {
        result.map_err(|reason| ProgramError::InvalidInput { line, reason })?;
        return Ok(());
    }
    if let Some(result) = services::apply_fake_directive(manager, fields) {
        result.map_err(|reason| ProgramError::InvalidInput { line, reason })?;
        return Ok(());
    }
    match fields {
        ["chat", client, source, name, message] => {
            let event = ClientEvent::Chat {
                client: parse_uuid(client, line, "chat client UUID is invalid")?,
                source: parse_uuid(source, line, "chat source UUID is invalid")?,
                name: clean_text(name, 256),
                message: clean_text(message, MAX_MESSAGE_BYTES),
                normal: true,
                fully_audible: true,
            };
            let _ = manager.process_event(event);
        }
        ["im", client, source, name, dialog, group, message, session] => {
            let dialog = match *dialog {
                "agent" => InstantMessageDialog::MessageFromAgent,
                "object" => InstantMessageDialog::MessageFromObject,
                "teleport" => InstantMessageDialog::RequestTeleport,
                _ => return invalid(line, "IM dialog must be agent, object, or teleport"),
            };
            let group = match *group {
                "true" => true,
                "false" => false,
                _ => return invalid(line, "IM group field must be true or false"),
            };
            let event = ClientEvent::InstantMessage {
                client: parse_uuid(client, line, "IM client UUID is invalid")?,
                source: parse_uuid(source, line, "IM source UUID is invalid")?,
                name: clean_text(name, 256),
                message: clean_text(message, MAX_COMMAND_BYTES),
                dialog,
                session: parse_uuid(session, line, "IM session UUID is invalid")?,
                group,
            };
            if let Some((command, from)) = manager.process_event(event) {
                manager.dispatch_line(&command, from).await;
            }
        }
        ["packet", client, packet_type, simulator, bytes] => {
            let bytes = bytes
                .parse::<usize>()
                .map_err(|_| ProgramError::InvalidInput {
                    line,
                    reason: "packet byte count is invalid",
                })?;
            let _ = manager.process_event(ClientEvent::Packet {
                client: parse_uuid(client, line, "packet client UUID is invalid")?,
                packet_type: clean_text(packet_type, 128),
                simulator: clean_text(simulator, 256),
                bytes,
            });
        }
        ["effect", client, summary] => {
            let _ = manager.process_event(ClientEvent::Effect {
                client: parse_uuid(client, line, "effect client UUID is invalid")?,
                summary: clean_text(summary, 1024),
            });
        }
        ["disconnect", client, reason] => {
            let _ = manager.process_event(ClientEvent::Disconnected {
                client: parse_uuid(client, line, "disconnect client UUID is invalid")?,
                reason: clean_text(reason, 512),
            });
        }
        ["cancel"] => manager.cancellation.cancel(),
        ["shutdown"] => {
            manager.running = false;
            manager.cancellation.cancel();
        }
        _ => {
            return invalid(
                line,
                "unknown fake directive; see --help and programs/README.md for supported records",
            );
        }
    }
    Ok(())
}

fn read_bounded_lines(path: &Path) -> Result<Vec<String>, ProgramError> {
    let file = File::open(path)
        .map_err(|source| input_error(format!("opening {}", path.display()), source))?;
    if file
        .metadata()
        .map_err(|source| input_error("reading input metadata", source))?
        .len()
        > MAX_INPUT_BYTES
    {
        return Err(ProgramError::InvalidInput {
            line: 0,
            reason: "input exceeds 1 MiB",
        });
    }
    let mut bytes = Vec::new();
    file.take(MAX_INPUT_BYTES + 1)
        .read_to_end(&mut bytes)
        .map_err(|source| input_error("reading input", source))?;
    if bytes.len() as u64 > MAX_INPUT_BYTES {
        return Err(ProgramError::InvalidInput {
            line: 0,
            reason: "input exceeds 1 MiB",
        });
    }
    let mut lines = Vec::new();
    for (index, line) in BufReader::new(bytes.as_slice()).lines().enumerate() {
        if lines.len() == MAX_INPUT_LINES {
            return Err(ProgramError::InvalidInput {
                line: index + 1,
                reason: "input exceeds 4,096 lines",
            });
        }
        lines.push(line.map_err(|source| input_error("decoding input as UTF-8", source))?);
    }
    Ok(lines)
}

fn parse_start_location(value: &str) -> Result<String, String> {
    if !value.contains('/') {
        return NetworkManager::start_location(value.into(), 128, 128, 40)
            .map_err(|_| "invalid start location".into());
    }
    let fields: Vec<_> = value.split('/').collect();
    let [region, x, y, z] = fields.as_slice() else {
        return Err("invalid start location".into());
    };
    let x = x.parse().map_err(|_| "invalid start X")?;
    let y = y.parse().map_err(|_| "invalid start Y")?;
    let z = z.parse().map_err(|_| "invalid start Z")?;
    NetworkManager::start_location((*region).into(), x, y, z)
        .map_err(|_| "invalid start location".into())
}

fn tokenize(input: &str) -> Result<Vec<String>, &'static str> {
    let mut tokens = Vec::new();
    let mut current = String::new();
    let mut quote = None;
    let mut escaped = false;
    for ch in input.chars() {
        if escaped {
            current.push(ch);
            escaped = false;
            continue;
        }
        if ch == '\\' {
            escaped = true;
            continue;
        }
        if let Some(delimiter) = quote {
            if ch == delimiter {
                quote = None;
            } else {
                current.push(ch);
            }
            continue;
        }
        match ch {
            '\'' | '"' => quote = Some(ch),
            _ if ch.is_whitespace() => {
                if !current.is_empty() {
                    tokens.push(std::mem::take(&mut current));
                }
            }
            _ => current.push(ch),
        }
    }
    if escaped {
        return Err("dangling backslash escape");
    }
    if quote.is_some() {
        return Err("unterminated quoted token");
    }
    if !current.is_empty() {
        tokens.push(current);
    }
    Ok(tokens)
}

fn valid_command_name(value: &str) -> bool {
    !value.is_empty()
        && value.len() <= 64
        && value
            .chars()
            .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
}

fn bounded_message(value: &str) -> String {
    truncate_utf8(&clean_text(value, MAX_MESSAGE_BYTES), MAX_MESSAGE_BYTES)
}

fn clean_text(value: &str, maximum: usize) -> String {
    let value: String = value
        .chars()
        .map(|ch| if ch.is_control() { ' ' } else { ch })
        .collect();
    truncate_utf8(value.trim(), maximum)
}

fn truncate_utf8(value: &str, maximum: usize) -> String {
    if value.len() <= maximum {
        return value.into();
    }
    let mut end = maximum;
    while !value.is_char_boundary(end) {
        end -= 1;
    }
    value[..end].into()
}

fn redact_text(value: &str) -> String {
    let lower = value.to_ascii_lowercase();
    if [
        "password=",
        "passwd=",
        "pass=",
        "authorization:",
        "authorization=",
        "capability=",
        "token=",
        "oauth",
    ]
    .iter()
    .any(|marker| lower.contains(marker))
    {
        return "<redacted>".into();
    }
    value
        .split_whitespace()
        .map(|word| {
            if word.contains("://") {
                "<redacted-url>"
            } else {
                word
            }
        })
        .collect::<Vec<_>>()
        .join(" ")
}

fn sanitize_remote_message(value: &str) -> String {
    match tokenize(value) {
        Ok(tokens)
            if tokens.first().is_some_and(|command| {
                matches!(command.to_ascii_lowercase().as_str(), "login" | "md5")
            }) =>
        {
            "<sensitive command redacted>".into()
        }
        _ => clean_text(value, MAX_COMMAND_BYTES),
    }
}

fn parse_uuid(value: &str, line: usize, reason: &'static str) -> Result<UUID, ProgramError> {
    UUID::new_with_string(value.into()).map_err(|_| ProgramError::InvalidInput { line, reason })
}

fn invalid<T>(line: usize, reason: &'static str) -> Result<T, ProgramError> {
    Err(ProgramError::InvalidInput { line, reason })
}

fn input_error(action: impl Into<String>, source: io::Error) -> ProgramError {
    ProgramError::Input {
        action: action.into(),
        source,
    }
}

fn try_event(sender: &mpsc::Sender<ClientEvent>, event: ClientEvent, dropped: &AtomicUsize) {
    if sender.try_send(event).is_err() {
        dropped.fetch_add(1, Ordering::Relaxed);
    }
}

fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
    mutex
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

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

    #[test]
    fn command_tokenizer_handles_quotes_escapes_and_errors() {
        assert_eq!(
            tokenize("say 7 'hello world' \"and \\\"quotes\\\"\"").unwrap(),
            ["say", "7", "hello world", "and \"quotes\""]
        );
        assert_eq!(tokenize("say one\\ two").unwrap(), ["say", "one two"]);
        assert!(tokenize("say 'unterminated").is_err());
        assert!(tokenize("say dangling\\").is_err());
    }

    #[test]
    fn issue_owned_targets_are_absent_from_pending_inventory() {
        let pending = pending_test_client_commands();
        assert_eq!(
            TEST_CLIENT_COMMANDS.len(),
            pending.len() + IMPLEMENTED_TEST_CLIENT_COMMANDS.len()
        );
        for command in IMPLEMENTED_TEST_CLIENT_COMMANDS {
            assert!(TEST_CLIENT_COMMANDS.contains(command));
            assert!(!pending.contains(command));
        }
        assert!(pending.is_empty());
    }

    #[test]
    fn start_locations_and_utf8_limits_are_deterministic() {
        assert!(
            parse_start_location("Sandbox/1/2/3")
                .unwrap()
                .contains("Sandbox")
        );
        assert!(parse_start_location("Sandbox/one/2/3").is_err());
        assert_eq!(truncate_utf8("ab☃cd", 4), "ab");
        assert_eq!(truncate_utf8("ab☃cd", 5), "ab☃");
    }
}
