//! Native login request and response models.
//!
//! Login credentials are deliberately excluded from every Debug view. The
//! request flow consumes owned parameters and clears secrets after building
//! the wire request.

#![allow(clippy::missing_errors_doc)]
#![allow(clippy::needless_pass_by_value)] // Owned parameters are fixed by the mapped C# API.
#![allow(clippy::too_many_arguments)] // Delegate signatures are fixed by the mapped C# API.
#![allow(clippy::too_many_lines)]
#![allow(clippy::type_complexity)] // Delegate signatures are fixed by the mapped C# API.

use crate::{
    BuddyListEntry, Error, GridClient, HomeInfo, InventoryFolder, LastExecStatus,
    LoginResponseDataParseMessageLevel, LoginState, LoginStatus,
};
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
use libremetaverse_types::compat::{ExternalError, Hashtable, Object};
use libremetaverse_types::{FolderType, UUID, Utils, Vector3};
use std::collections::HashMap;
use std::fmt;
use std::net::IpAddr;
use std::sync::{Arc, Mutex, OnceLock, RwLock};
use std::time::{Duration, Instant};

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

fn read<T>(value: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
    value
        .read()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

fn write<T>(value: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
    value
        .write()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

#[derive(Clone)]
pub struct LoginCredential {
    first_name: String,
    last_name: String,
    password: String,
    token: String,
    mfa_hash: String,
}

impl fmt::Debug for LoginCredential {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("LoginCredential")
            .field("first_name", &self.first_name)
            .field("last_name", &self.last_name)
            .field("password", &"<redacted>")
            .field("token", &"<redacted>")
            .field("mfa_hash", &"<redacted>")
            .finish()
    }
}

impl LoginCredential {
    pub fn new_with_string_string_string(
        first: String,
        last: String,
        passwd: String,
    ) -> Result<Self, Error> {
        Self::new_with_string_string_string_string_string(
            first,
            last,
            passwd,
            String::new(),
            String::new(),
        )
    }

    pub fn new_with_string_string_string_string_string(
        first: String,
        last: String,
        passwd: String,
        token: String,
        mfa_hash: String,
    ) -> Result<Self, Error> {
        Ok(Self {
            first_name: first,
            last_name: last,
            password: passwd,
            token,
            mfa_hash,
        })
    }

    #[must_use]
    pub fn first_name(&self) -> String {
        self.first_name.clone()
    }
    #[must_use]
    pub fn last_name(&self) -> String {
        self.last_name.clone()
    }
    #[must_use]
    pub fn password(&self) -> String {
        self.password.clone()
    }
    #[must_use]
    pub fn token(&self) -> String {
        self.token.clone()
    }
    #[must_use]
    pub fn mfa_hash(&self) -> String {
        self.mfa_hash.clone()
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LoginException {
    message: String,
    inner: Option<ExternalError>,
}

impl LoginException {
    pub fn new_with_string(message: String) -> Result<Self, Error> {
        Ok(Self {
            message,
            inner: None,
        })
    }

    pub fn new_with_string_exception(
        message: String,
        inner_exception: ExternalError,
    ) -> Result<Self, Error> {
        Ok(Self {
            message,
            inner: Some(inner_exception),
        })
    }
}

#[derive(Clone)]
pub struct LoginParams {
    pub agree_to_tos: bool,
    pub author: String,
    pub channel: String,
    pub first_name: String,
    pub id0: String,
    pub last_exec_event: LastExecStatus,
    pub last_name: String,
    pub login_location: String,
    pub mac: String,
    pub method_name: String,
    pub mfa_enabled: bool,
    pub mfa_hash: String,
    pub options: Vec<String>,
    pub password: String,
    pub platform: String,
    pub platform_version: String,
    pub read_critical: bool,
    pub start: String,
    pub timeout: i32,
    pub token: String,
    pub uri: String,
    pub user_agent: String,
    pub version: String,
    pub viewer_digest: String,
}

impl fmt::Debug for LoginParams {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("LoginParams")
            .field("uri", &redact_uri(&self.uri))
            .field("timeout", &self.timeout)
            .field("method_name", &self.method_name)
            .field("first_name", &self.first_name)
            .field("last_name", &self.last_name)
            .field("password", &"<redacted>")
            .field("token", &"<redacted>")
            .field("mfa_hash", &"<redacted>")
            .field("start", &self.start)
            .field("channel", &self.channel)
            .field("version", &self.version)
            .field("options", &self.options)
            .finish_non_exhaustive()
    }
}

impl LoginParams {
    pub fn new_with_constructor() -> Result<Self, Error> {
        let hashed = hashed_mac()?;
        Ok(Self {
            agree_to_tos: true,
            author: String::new(),
            channel: String::new(),
            first_name: String::new(),
            id0: hashed.clone(),
            last_exec_event: LastExecStatus::Normal,
            last_name: String::new(),
            login_location: String::new(),
            mac: hashed,
            method_name: "login_to_simulator".to_owned(),
            mfa_enabled: false,
            mfa_hash: String::new(),
            options: default_options(),
            password: String::new(),
            platform: platform(),
            platform_version: platform_version(),
            read_critical: true,
            start: "last".to_owned(),
            timeout: 0,
            token: String::new(),
            uri: String::new(),
            user_agent: String::new(),
            version: String::new(),
            viewer_digest: String::new(),
        })
    }

    pub fn new_with_grid_client_login_credential_string_string(
        client: GridClient,
        credential: LoginCredential,
        channel: String,
        version: String,
    ) -> Result<Self, Error> {
        let mut result = Self::new_with_constructor()?;
        result
            .uri
            .clone_from(&client.settings_ref().connection.login_server);
        result.timeout = client.settings_ref().timing.login_timeout;
        result.mfa_enabled = client.settings_ref().connection.mfa_enabled;
        result.first_name = credential.first_name;
        result.last_name = credential.last_name;
        result.password = credential.password;
        result.token = credential.token;
        result.mfa_hash = credential.mfa_hash;
        result.channel = channel;
        result.version = version;
        Ok(result)
    }

    pub fn new_with_grid_client_login_credential_string_string_string(
        client: GridClient,
        credential: LoginCredential,
        channel: String,
        version: String,
        login_uri: String,
    ) -> Result<Self, Error> {
        let mut result = Self::new_with_grid_client_login_credential_string_string(
            client, credential, channel, version,
        )?;
        result.uri = login_uri;
        Ok(result)
    }

    pub fn new_with_grid_client_string_string_string_string_string(
        client: GridClient,
        first_name: String,
        last_name: String,
        password: String,
        channel: String,
        version: String,
    ) -> Result<Self, Error> {
        Self::new_with_grid_client_login_credential_string_string(
            client,
            LoginCredential::new_with_string_string_string(first_name, last_name, password)?,
            channel,
            version,
        )
    }

    pub fn new_with_grid_client_string_string_string_string_string_string(
        client: GridClient,
        first_name: String,
        last_name: String,
        password: String,
        channel: String,
        version: String,
        login_uri: String,
    ) -> Result<Self, Error> {
        let mut result = Self::new_with_grid_client_string_string_string_string_string(
            client, first_name, last_name, password, channel, version,
        )?;
        result.uri = login_uri;
        Ok(result)
    }

    pub(crate) fn clear_secrets(&mut self) {
        self.password.clear();
        self.token.clear();
        self.mfa_hash.clear();
    }
}

fn default_options() -> Vec<String> {
    [
        "inventory-root",
        "inventory-skeleton",
        "initial-outfit",
        "gestures",
        "display_names",
        "event_categories",
        "event_notifications",
        "classified_categories",
        "adult_compliant",
        "buddy-list",
        "newuser-config",
        "ui-config",
        "advanced-mode",
        "max-agent-groups",
        "map-server-url",
        "voice-config",
        "tutorial_settings",
        "login-flags",
        "global-textures",
    ]
    .into_iter()
    .map(str::to_owned)
    .collect()
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LoginProgressEventArgs {
    status: LoginStatus,
    message: String,
    fail_reason: String,
}

impl LoginProgressEventArgs {
    pub fn new(login: LoginStatus, message: String, fail_reason: String) -> Result<Self, Error> {
        Ok(Self {
            status: login,
            message,
            fail_reason,
        })
    }

    #[must_use]
    pub fn status(&self) -> LoginStatus {
        self.status
    }
    #[must_use]
    pub fn message(&self) -> String {
        self.message.clone()
    }
    #[must_use]
    pub fn fail_reason(&self) -> String {
        self.fail_reason.clone()
    }
}

#[derive(Clone, PartialEq)]
pub struct AccountLevelBenefits {
    integers: HashMap<&'static str, i32>,
    estate_access_token: String,
    linden_buy_fee: f64,
    lastname_change_cost: f64,
    large_texture_upload_cost: Vec<i32>,
}

impl fmt::Debug for AccountLevelBenefits {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("AccountLevelBenefits")
            .field("integers", &self.integers)
            .field("estate_access_token", &"[REDACTED]")
            .field("linden_buy_fee", &self.linden_buy_fee)
            .field("lastname_change_cost", &self.lastname_change_cost)
            .field("large_texture_upload_cost", &self.large_texture_upload_cost)
            .finish()
    }
}

const BENEFIT_INTEGER_KEYS: &[&str] = &[
    "mesh_upload_cost",
    "one_time_event_allowed",
    "repeating_events_cost",
    "lastname_change_rate",
    "marketplace_concierge_support",
    "phone_support",
    "partner_fee",
    "create_repeating_events",
    "texture_upload_cost",
    "priority_entry",
    "object_account_level",
    "use_animesh",
    "beta_grid_land",
    "create_group_cost",
    "live_chat",
    "land_auctions_allowed",
    "mainland_tier",
    "local_experiences",
    "one_time_event_cost",
    "marketplace_ple_limit",
    "unpartner_fee",
    "group_membership_limit",
    "animation_upload_cost",
    "script_limit",
    "transaction_history_limit",
    "animated_object_limit",
    "gridwide_experience_limit",
    "lastname_change_allowed",
    "sound_upload_cost",
    "premium_alts",
    "picks_limit",
    "place_pages",
    "premium_access",
    "marketplace_listing_limit",
    "premium_gifts",
    "voice_morphing",
    "stored_im_limit",
    "stipend",
    "signup_bonus",
    "attachment_limit",
];

impl AccountLevelBenefits {
    pub fn new_with_osd_map(reply: OSDMap) -> Result<Self, Error> {
        Self::from_map(&reply.snapshot())
    }

    pub fn new_with_hashtable(reply: Hashtable) -> Result<Self, Error> {
        Self::from_map(&hashtable_to_map(&reply)?)
    }

    fn from_map(reply: &HashMap<String, OSD>) -> Result<Self, Error> {
        let mut integers = HashMap::new();
        for &key in BENEFIT_INTEGER_KEYS {
            let default = if key == "attachment_limit" { 38 } else { -1 };
            let value = reply
                .get(key)
                .map(OSD::as_integer)
                .transpose()?
                .unwrap_or(default);
            integers.insert(key, value);
        }
        let mut large_texture_upload_cost = match reply.get("large_texture_upload_cost") {
            Some(OSD::Array(values)) => values
                .iter()
                .map(OSD::as_integer)
                .collect::<Result<Vec<_>, _>>()?,
            _ => Vec::new(),
        };
        if large_texture_upload_cost.is_empty() {
            large_texture_upload_cost.push(integers["texture_upload_cost"]);
        } else {
            large_texture_upload_cost.sort_unstable();
        }
        Ok(Self {
            integers,
            estate_access_token: osd_string(reply, "estate_access_token")?,
            linden_buy_fee: osd_real_or(reply, "linden_buy_fee", -1.0)?,
            lastname_change_cost: osd_real_or(reply, "lastname_change_cost", -1.0)?,
            large_texture_upload_cost,
        })
    }

    fn integer(&self, key: &'static str) -> i32 {
        self.integers.get(key).copied().unwrap_or(-1)
    }

    #[must_use]
    pub fn estate_access_token(&self) -> String {
        self.estate_access_token.clone()
    }
    #[must_use]
    pub const fn linden_buy_fee(&self) -> f64 {
        self.linden_buy_fee
    }
    #[must_use]
    pub const fn lastname_change_cost(&self) -> f64 {
        self.lastname_change_cost
    }
    #[must_use]
    pub fn large_texture_upload_cost(&self) -> Vec<i32> {
        self.large_texture_upload_cost.clone()
    }
}

macro_rules! benefit_getters {
    ($($method:ident => $key:literal),+ $(,)?) => {$(
        #[must_use]
        pub fn $method(&self) -> i32 { self.integer($key) }
    )+};
}

impl AccountLevelBenefits {
    benefit_getters! {
        animated_object_limit => "animated_object_limit",
        animation_upload_cost => "animation_upload_cost",
        attachment_limit => "attachment_limit",
        beta_grid_land => "beta_grid_land",
        create_group_cost => "create_group_cost",
        create_repeating_events => "create_repeating_events",
        gridwide_experience_limit => "gridwide_experience_limit",
        group_membership_limit => "group_membership_limit",
        land_auctions_allowed => "land_auctions_allowed",
        lastname_change_allowed => "lastname_change_allowed",
        lastname_change_rate => "lastname_change_rate",
        live_chat => "live_chat",
        local_experiences => "local_experiences",
        mainland_tier => "mainland_tier",
        marketplace_concierge_support => "marketplace_concierge_support",
        marketplace_listing_limit => "marketplace_listing_limit",
        marketplace_ple_limit => "marketplace_ple_limit",
        mesh_upload_cost => "mesh_upload_cost",
        object_account_level => "object_account_level",
        one_time_event_allowed => "one_time_event_allowed",
        one_time_event_cost => "one_time_event_cost",
        partner_fee => "partner_fee",
        phone_support => "phone_support",
        picks_limit => "picks_limit",
        place_pages => "place_pages",
        premium_access => "premium_access",
        premium_alts => "premium_alts",
        premium_gifts => "premium_gifts",
        priority_entry => "priority_entry",
        repeating_events_cost => "repeating_events_cost",
        script_limit => "script_limit",
        signup_bonus => "signup_bonus",
        sound_upload_cost => "sound_upload_cost",
        stipend => "stipend",
        stored_im_limit => "stored_im_limit",
        texture_upload_cost => "texture_upload_cost",
        transaction_history_limit => "transaction_history_limit",
        unpartner_fee => "unpartner_fee",
        use_animesh => "use_animesh",
        voice_morphing => "voice_morphing",
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LoginResponseDataParseMessage {
    level: LoginResponseDataParseMessageLevel,
    message: String,
    exception: Option<ExternalError>,
}

impl LoginResponseDataParseMessage {
    pub fn new(
        level: LoginResponseDataParseMessageLevel,
        message: String,
        ex: Option<ExternalError>,
    ) -> Result<Self, Error> {
        Ok(Self {
            level,
            message,
            exception: ex,
        })
    }

    #[must_use]
    pub fn level(&self) -> LoginResponseDataParseMessageLevel {
        self.level
    }
    #[must_use]
    pub fn message(&self) -> String {
        self.message.clone()
    }
    #[must_use]
    pub fn exception(&self) -> Option<ExternalError> {
        self.exception.clone()
    }
}

#[derive(Clone, Default)]
pub struct LoginResponseDataParseResult {
    inner: Arc<Mutex<ParseResultFields>>,
}

#[derive(Clone, Default)]
struct ParseResultFields {
    success: bool,
    messages: Vec<LoginResponseDataParseMessage>,
    parsed_data: Option<LoginResponseData>,
}

impl LoginResponseDataParseResult {
    pub fn new() -> Result<Self, Error> {
        Ok(Self::default())
    }

    pub fn add(
        &self,
        level: LoginResponseDataParseMessageLevel,
        message: String,
        ex: Option<ExternalError>,
    ) -> Result<(), Error> {
        mutex(&self.inner)
            .messages
            .push(LoginResponseDataParseMessage::new(level, message, ex)?);
        Ok(())
    }

    #[must_use]
    pub fn messages(&self) -> Vec<LoginResponseDataParseMessage> {
        mutex(&self.inner).messages.clone()
    }
    #[must_use]
    pub fn parsed_data(&self) -> Option<LoginResponseData> {
        mutex(&self.inner).parsed_data.clone()
    }
    pub fn set_parsed_data(&mut self, value: Option<LoginResponseData>) {
        mutex(&self.inner).parsed_data = value;
    }
    #[must_use]
    pub fn success(&self) -> bool {
        mutex(&self.inner).success
    }
    pub fn set_success(&mut self, value: bool) {
        mutex(&self.inner).success = value;
    }
}

#[derive(Clone)]
pub struct LoginResponseData(Arc<RwLock<LoginResponseFields>>);

#[derive(Clone)]
struct LoginResponseFields {
    account_level_benefits: Option<AccountLevelBenefits>,
    account_type: String,
    agent_access: String,
    agent_access_max: String,
    agent_appearance_service_url: String,
    agent_id: UUID,
    agent_region_access: String,
    buddy_list: Vec<BuddyListEntry>,
    cof_version: u32,
    circuit_code: i32,
    classified_categories: Vec<Object>,
    event_categories: Vec<Object>,
    first_login: bool,
    first_name: String,
    gestures: HashMap<UUID, UUID>,
    global_textures: Vec<Object>,
    home: HomeInfo,
    initial_outfit: String,
    inventory_root: UUID,
    inventory_skeleton: Vec<InventoryFolder>,
    last_name: String,
    library_owner: UUID,
    library_root: UUID,
    library_skeleton: Vec<InventoryFolder>,
    login: LoginState,
    look_at: Vector3,
    map_server_url: String,
    max_agent_groups: i32,
    message: String,
    mfa_hash: String,
    next_duration: i32,
    next_method: String,
    next_options: Vec<String>,
    next_url: String,
    open_id_url: String,
    premium_packages: HashMap<String, Option<Object>>,
    reason: String,
    region_size_x: u32,
    region_size_y: u32,
    region_x: u32,
    region_y: u32,
    seconds_since_epoch: i32,
    secure_session_id: UUID,
    seed_capability: String,
    session_id: UUID,
    sim_ip: Option<IpAddr>,
    sim_port: u16,
    snapshot_config_url: String,
    start_location: String,
    udp_blacklist: String,
    ui_config: Vec<Object>,
}

impl Default for LoginResponseFields {
    fn default() -> Self {
        Self {
            account_level_benefits: None,
            account_type: String::new(),
            agent_access: String::new(),
            agent_access_max: String::new(),
            agent_appearance_service_url: String::new(),
            agent_id: UUID::zero(),
            agent_region_access: String::new(),
            buddy_list: Vec::new(),
            cof_version: 0,
            circuit_code: 0,
            classified_categories: Vec::new(),
            event_categories: Vec::new(),
            first_login: false,
            first_name: String::new(),
            gestures: HashMap::new(),
            global_textures: Vec::new(),
            home: HomeInfo {
                look_at: Vector3::zero(),
                position: Vector3::zero(),
                region_handle: 0,
            },
            initial_outfit: String::new(),
            inventory_root: UUID::zero(),
            inventory_skeleton: Vec::new(),
            last_name: String::new(),
            library_owner: UUID::zero(),
            library_root: UUID::zero(),
            library_skeleton: Vec::new(),
            login: LoginState::False,
            look_at: Vector3::zero(),
            map_server_url: String::new(),
            max_agent_groups: -1,
            message: String::new(),
            mfa_hash: String::new(),
            next_duration: 0,
            next_method: String::new(),
            next_options: Vec::new(),
            next_url: String::new(),
            open_id_url: String::new(),
            premium_packages: HashMap::new(),
            reason: String::new(),
            region_size_x: crate::network_manager::Simulator::DEFAULT_REGION_SIZE_X,
            region_size_y: crate::network_manager::Simulator::DEFAULT_REGION_SIZE_Y,
            region_x: 0,
            region_y: 0,
            seconds_since_epoch: 0,
            secure_session_id: UUID::zero(),
            seed_capability: String::new(),
            session_id: UUID::zero(),
            sim_ip: None,
            sim_port: 0,
            snapshot_config_url: String::new(),
            start_location: String::new(),
            udp_blacklist: String::new(),
            ui_config: Vec::new(),
        }
    }
}

impl fmt::Debug for LoginResponseData {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let fields = read(&self.0);
        formatter
            .debug_struct("LoginResponseData")
            .field("login", &fields.login)
            .field("reason", &fields.reason)
            .field("message", &fields.message)
            .field("agent_id", &fields.agent_id)
            .field("session_id", &"<redacted>")
            .field("secure_session_id", &"<redacted>")
            .field("mfa_hash", &"<redacted>")
            .field("seed_capability", &"<redacted>")
            .field("sim_ip", &fields.sim_ip)
            .field("sim_port", &fields.sim_port)
            .finish_non_exhaustive()
    }
}

impl LoginResponseData {
    pub fn new() -> Result<Self, Error> {
        Ok(Self(Arc::new(RwLock::new(LoginResponseFields::default()))))
    }

    pub(crate) fn copy_from(&self, other: &Self) {
        if Arc::ptr_eq(&self.0, &other.0) {
            return;
        }
        *write(&self.0) = read(&other.0).clone();
    }

    pub(crate) fn clear_secrets(&self) {
        let mut fields = write(&self.0);
        fields.session_id = UUID::zero();
        fields.secure_session_id = UUID::zero();
        fields.mfa_hash.clear();
        fields.seed_capability.clear();
        fields.next_url.clear();
        fields.account_level_benefits = None;
    }

    pub fn parse_with_osd_map(&self, reply: OSDMap) -> Result<LoginResponseDataParseResult, Error> {
        self.parse_map(reply.snapshot(), false)
    }

    pub fn parse_with_hashtable(
        &self,
        reply: Hashtable,
    ) -> Result<LoginResponseDataParseResult, Error> {
        self.parse_map(hashtable_to_map(&reply)?, true)
    }

    fn parse_map(
        &self,
        reply: HashMap<String, OSD>,
        early_return_if_not_success: bool,
    ) -> Result<LoginResponseDataParseResult, Error> {
        let mut parsed = LoginResponseFields {
            agent_id: osd_uuid(&reply, "agent_id")?,
            session_id: osd_uuid(&reply, "session_id")?,
            secure_session_id: osd_uuid(&reply, "secure_session_id")?,
            first_name: osd_string(&reply, "first_name")?
                .trim_matches('"')
                .trim()
                .to_owned(),
            last_name: osd_string(&reply, "last_name")?
                .trim_matches('"')
                .trim()
                .to_owned(),
            start_location: osd_string(&reply, "start_location")?,
            agent_access: osd_string(&reply, "agent_access")?,
            agent_access_max: osd_string(&reply, "agent_access_max")?,
            agent_region_access: osd_string(&reply, "agent_region_access")?,
            look_at: osd_vector3(&reply, "look_at")?,
            reason: osd_string(&reply, "reason")?,
            message: osd_string(&reply, "message")?,
            ..LoginResponseFields::default()
        };
        let login = osd_string(&reply, "login")?;
        parsed.login = if login.eq_ignore_ascii_case("indeterminate") {
            LoginState::Indeterminate
        } else if login.eq_ignore_ascii_case("true")
            || reply
                .get("login")
                .is_some_and(|value| value.as_boolean().unwrap_or(false))
        {
            LoginState::True
        } else {
            LoginState::False
        };
        parsed.next_url = osd_string(&reply, "next_url")?;
        parsed.next_duration =
            i32::try_from(osd_uint(&reply, "next_duration")?).unwrap_or(i32::MAX);
        parsed.next_method = osd_string(&reply, "next_method")?;
        parsed.next_options = osd_string_array(&reply, "next_options")?;
        if early_return_if_not_success && parsed.login != LoginState::True {
            *write(&self.0) = parsed;
            return Ok(parse_result_for(self));
        }

        parsed.home = parse_home(&reply)?;
        parsed.circuit_code = osd_uint(&reply, "circuit_code")?.cast_signed();
        parsed.region_x = osd_uint(&reply, "region_x")?;
        parsed.region_y = osd_uint(&reply, "region_y")?;
        parsed.region_size_x = optional_uint(&reply, "region_size_x")?
            .unwrap_or(crate::network_manager::Simulator::DEFAULT_REGION_SIZE_X);
        parsed.region_size_y = optional_uint(&reply, "region_size_y")?
            .unwrap_or(crate::network_manager::Simulator::DEFAULT_REGION_SIZE_Y);
        parsed.sim_port = u16::try_from(osd_uint(&reply, "sim_port")?).unwrap_or(0);
        parsed.sim_ip = osd_string(&reply, "sim_ip")?.parse().ok();
        parsed.seed_capability = osd_string(&reply, "seed_capability")?;
        parsed.seconds_since_epoch = osd_uint(&reply, "seconds_since_epoch")?.cast_signed();
        parsed.buddy_list = parse_buddy_list(&reply)?;
        parsed.inventory_root = parse_mapped_uuid_map("inventory-root", "folder_id", &reply)?;
        parsed.library_owner = parse_mapped_uuid_map("inventory-lib-owner", "agent_id", &reply)?;
        parsed.library_root = parse_mapped_uuid_map("inventory-lib-root", "folder_id", &reply)?;
        parsed.inventory_skeleton =
            parse_inventory_skeleton_map("inventory-skeleton", parsed.agent_id, &reply)?;
        parsed.library_skeleton =
            parse_inventory_skeleton_map("inventory-skel-lib", parsed.library_owner, &reply)?;
        parsed.account_type = osd_string(&reply, "account_type")?;
        parsed.agent_appearance_service_url = osd_string(&reply, "agent_appearance_service")?;
        parsed.cof_version = osd_uint(&reply, "cof_version")?;
        parsed.map_server_url = osd_string(&reply, "map-server-url")?;
        parsed.open_id_url = osd_string(&reply, "openid_url")?;
        parsed.snapshot_config_url = osd_string(&reply, "snapshot_config_url")?;
        parsed.udp_blacklist = osd_string(&reply, "udp_blacklist")?;
        parsed.mfa_hash = osd_string(&reply, "mfa_hash")?;
        if let Some(OSD::Map(map)) = reply.get("account_level_benefits") {
            parsed.account_level_benefits = Some(AccountLevelBenefits::from_map(map)?);
        }
        parsed.classified_categories = osd_object_array(&reply, "classified_categories")?;
        parsed.event_categories = osd_object_array(&reply, "event_categories")?;
        parsed.global_textures = osd_object_array(&reply, "global-textures")?;
        parsed.ui_config = osd_object_array(&reply, "ui-config")?;
        if let Some(OSD::Map(map)) = reply.get("premium_packages") {
            parsed.premium_packages = map
                .iter()
                .map(|(key, value)| Ok((key.clone(), Some(osd_to_object(value.clone())?))))
                .collect::<Result<_, Error>>()?;
        }
        parsed.max_agent_groups = optional_uint(&reply, "max-agent-groups")?
            .map_or(-1, |value| i32::try_from(value).unwrap_or(i32::MAX));
        parsed.initial_outfit = match reply.get("initial-outfit") {
            Some(OSD::Array(items)) => items
                .iter()
                .filter_map(|item| match item {
                    OSD::Map(map) => map.get("item_id"),
                    _ => None,
                })
                .map(OSD::as_string)
                .collect::<Result<Vec<_>, _>>()?
                .join(","),
            _ => String::new(),
        };
        *write(&self.0) = parsed;
        Ok(parse_result_for(self))
    }

    pub fn parse_inventory_folders(
        key: String,
        owner: UUID,
        reply: OSDMap,
    ) -> Result<Vec<InventoryFolder>, Error> {
        parse_inventory_skeleton_map(&key, owner, &reply.snapshot())
    }

    pub fn parse_inventory_skeleton_with_string_osd_map(
        &self,
        key: String,
        reply: OSDMap,
    ) -> Result<Vec<InventoryFolder>, Error> {
        let owner = if key == "inventory-skel-lib" {
            self.library_owner()
        } else {
            self.agent_id()
        };
        parse_inventory_skeleton_map(&key, owner, &reply.snapshot())
    }

    pub fn parse_inventory_skeleton_with_string_hashtable(
        &self,
        key: String,
        reply: Hashtable,
    ) -> Result<Vec<InventoryFolder>, Error> {
        let owner = if key == "inventory-skel-lib" {
            self.library_owner()
        } else {
            self.agent_id()
        };
        parse_inventory_skeleton_map(&key, owner, &hashtable_to_map(&reply)?)
    }

    pub fn parse_mapped_uuid_with_string_string_osd_map(
        key: String,
        key2: String,
        reply: OSDMap,
    ) -> Result<UUID, Error> {
        parse_mapped_uuid_map(&key, &key2, &reply.snapshot())
    }

    pub fn parse_mapped_uuid_with_string_string_hashtable(
        key: String,
        key2: String,
        reply: Hashtable,
    ) -> Result<UUID, Error> {
        parse_mapped_uuid_map(&key, &key2, &hashtable_to_map(&reply)?)
    }

    pub fn parse_string_with_string_osd_map(key: String, reply: OSDMap) -> Result<String, Error> {
        osd_string(&reply.snapshot(), &key)
    }
    pub fn parse_string_with_string_hashtable(
        key: String,
        reply: Hashtable,
    ) -> Result<String, Error> {
        osd_string(&hashtable_to_map(&reply)?, &key)
    }
    pub fn parse_u_int_with_string_osd_map(key: String, reply: OSDMap) -> Result<u32, Error> {
        osd_uint(&reply.snapshot(), &key)
    }
    pub fn parse_u_int_with_string_hashtable(key: String, reply: Hashtable) -> Result<u32, Error> {
        osd_uint(&hashtable_to_map(&reply)?, &key)
    }
    pub fn parse_uuid_with_string_osd_map(key: String, reply: OSDMap) -> Result<UUID, Error> {
        osd_uuid(&reply.snapshot(), &key)
    }
    pub fn parse_uuid_with_string_hashtable(key: String, reply: Hashtable) -> Result<UUID, Error> {
        osd_uuid(&hashtable_to_map(&reply)?, &key)
    }
    pub fn parse_vector3_with_string_osd_map(key: String, reply: OSDMap) -> Result<Vector3, Error> {
        osd_vector3(&reply.snapshot(), &key)
    }
    pub fn parse_vector3_with_string_hashtable(
        key: String,
        reply: Hashtable,
    ) -> Result<Vector3, Error> {
        osd_vector3(&hashtable_to_map(&reply)?, &key)
    }
}

fn parse_result_for(data: &LoginResponseData) -> LoginResponseDataParseResult {
    LoginResponseDataParseResult {
        inner: Arc::new(Mutex::new(ParseResultFields {
            success: data.success(),
            messages: Vec::new(),
            parsed_data: Some(data.clone()),
        })),
    }
}

macro_rules! response_accessors {
    ($($get:ident, $set:ident, $field:ident, $ty:ty),+ $(,)?) => {$(
        #[must_use]
        pub fn $get(&self) -> $ty { read(&self.0).$field.clone() }
        pub fn $set(&mut self, value: $ty) { write(&self.0).$field = value; }
    )+};
}

impl LoginResponseData {
    response_accessors! {
        account_level_benefits, set_account_level_benefits, account_level_benefits, Option<AccountLevelBenefits>,
        account_type, set_account_type, account_type, String,
        agent_access, set_agent_access, agent_access, String,
        agent_access_max, set_agent_access_max, agent_access_max, String,
        agent_appearance_service_url, set_agent_appearance_service_url, agent_appearance_service_url, String,
        agent_id, set_agent_id, agent_id, UUID,
        agent_region_access, set_agent_region_access, agent_region_access, String,
        buddy_list, set_buddy_list, buddy_list, Vec<BuddyListEntry>,
        cof_version, set_cof_version, cof_version, u32,
        circuit_code, set_circuit_code, circuit_code, i32,
        classified_categories, set_classified_categories, classified_categories, Vec<Object>,
        event_categories, set_event_categories, event_categories, Vec<Object>,
        first_login, set_first_login, first_login, bool,
        first_name, set_first_name, first_name, String,
        gestures, set_gestures, gestures, HashMap<UUID, UUID>,
        global_textures, set_global_textures, global_textures, Vec<Object>,
        initial_outfit, set_initial_outfit, initial_outfit, String,
        inventory_root, set_inventory_root, inventory_root, UUID,
        inventory_skeleton, set_inventory_skeleton, inventory_skeleton, Vec<InventoryFolder>,
        last_name, set_last_name, last_name, String,
        library_owner, set_library_owner, library_owner, UUID,
        library_root, set_library_root, library_root, UUID,
        library_skeleton, set_library_skeleton, library_skeleton, Vec<InventoryFolder>,
        login, set_login, login, LoginState,
        look_at, set_look_at, look_at, Vector3,
        map_server_url, set_map_server_url, map_server_url, String,
        max_agent_groups, set_max_agent_groups, max_agent_groups, i32,
        message, set_message, message, String,
        mfa_hash, set_mfa_hash, mfa_hash, String,
        next_duration, set_next_duration, next_duration, i32,
        next_method, set_next_method, next_method, String,
        next_options, set_next_options, next_options, Vec<String>,
        next_url, set_next_url, next_url, String,
        open_id_url, set_open_id_url, open_id_url, String,
        premium_packages, set_premium_packages, premium_packages, HashMap<String, Option<Object>>,
        reason, set_reason, reason, String,
        region_size_x, set_region_size_x, region_size_x, u32,
        region_size_y, set_region_size_y, region_size_y, u32,
        region_x, set_region_x, region_x, u32,
        region_y, set_region_y, region_y, u32,
        seconds_since_epoch, set_seconds_since_epoch, seconds_since_epoch, i32,
        secure_session_id, set_secure_session_id, secure_session_id, UUID,
        seed_capability, set_seed_capability, seed_capability, String,
        session_id, set_session_id, session_id, UUID,
        sim_ip, set_sim_ip, sim_ip, Option<IpAddr>,
        sim_port, set_sim_port, sim_port, u16,
        snapshot_config_url, set_snapshot_config_url, snapshot_config_url, String,
        start_location, set_start_location, start_location, String,
        udp_blacklist, set_udp_blacklist, udp_blacklist, String,
        ui_config, set_ui_config, ui_config, Vec<Object>,
    }

    #[must_use]
    pub fn home(&self) -> HomeInfo {
        read(&self.0).home
    }
    #[must_use]
    pub fn success(&self) -> bool {
        self.login() == LoginState::True
    }
}

#[derive(Clone)]
pub struct NetworkManagerLoginResponseCallback {
    handler: Arc<dyn Fn(bool, bool, String, String, Option<LoginResponseData>) + Send + Sync>,
}

impl fmt::Debug for NetworkManagerLoginResponseCallback {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("NetworkManagerLoginResponseCallback")
            .finish_non_exhaustive()
    }
}

impl NetworkManagerLoginResponseCallback {
    pub fn from_handler(
        handler: impl Fn(bool, bool, String, String, Option<LoginResponseData>) + Send + Sync + 'static,
    ) -> Self {
        Self {
            handler: Arc::new(handler),
        }
    }

    pub fn new(object: Object, method: isize) -> Result<Self, Error> {
        if method == 0 {
            return Err(Error::Argument);
        }
        object
            .downcast_arc::<Self>()
            .map(|callback| (*callback).clone())
            .ok_or(Error::Argument)
    }

    pub fn invoke(
        &self,
        login_success: bool,
        redirect: bool,
        message: String,
        reason: String,
        reply_data: Option<LoginResponseData>,
    ) -> Result<(), Error> {
        let handler = Arc::clone(&self.handler);
        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            handler(login_success, redirect, message, reason, reply_data);
        }))
        .map_err(|_| Error::InvalidOperation)
    }

    pub fn begin_invoke(
        &self,
        login_success: bool,
        redirect: bool,
        message: String,
        reason: String,
        reply_data: Option<LoginResponseData>,
        callback: Box<dyn Fn(&dyn std::any::Any) + Send + Sync>,
        object: Object,
    ) -> Result<Box<dyn std::any::Any + Send + Sync>, Error> {
        self.invoke(login_success, redirect, message, reason, reply_data)?;
        callback(&object);
        Ok(Box::new(()))
    }

    pub fn end_invoke(&self, result: Box<dyn std::any::Any + Send + Sync>) -> Result<(), Error> {
        result
            .downcast::<()>()
            .map(|_| ())
            .map_err(|_| Error::Argument)
    }

    pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
        Arc::ptr_eq(&self.handler, &other.handler)
    }
}

pub(crate) fn start_location(sim: &str, x: i32, y: i32, z: i32) -> String {
    format!("uri:{sim}&{x}&{y}&{z}")
}

pub(crate) fn normalize_start(value: &str) -> Result<String, Error> {
    let value = value.trim();
    if value == "home" || value == "last" {
        return Ok(value.to_owned());
    }
    if value.is_empty() {
        return Err(Error::Argument);
    }
    let location = value.strip_prefix("secondlife://").unwrap_or(value);
    if let Some(legacy) = location.strip_prefix("uri:") {
        let mut fields = legacy.split('&');
        let sim = fields
            .next()
            .filter(|value| !value.is_empty())
            .ok_or(Error::Argument)?;
        let x = fields
            .next()
            .and_then(|value| value.parse().ok())
            .unwrap_or(128);
        let y = fields
            .next()
            .and_then(|value| value.parse().ok())
            .unwrap_or(128);
        let z = fields
            .next()
            .and_then(|value| value.parse().ok())
            .unwrap_or(0);
        return Ok(start_location(sim, x, y, z));
    }
    let path = location
        .split('?')
        .next()
        .unwrap_or(location)
        .trim_end_matches('/');
    let mut fields = path.split('/');
    let sim = fields
        .next()
        .filter(|value| !value.is_empty())
        .ok_or(Error::Argument)?;
    let x = fields
        .next()
        .and_then(|value| value.parse().ok())
        .unwrap_or(128);
    let y = fields
        .next()
        .and_then(|value| value.parse().ok())
        .unwrap_or(128);
    let z = fields
        .next()
        .and_then(|value| value.parse().ok())
        .unwrap_or(0);
    Ok(start_location(sim, x, y, z))
}

#[must_use]
pub(crate) fn platform() -> String {
    match std::env::consts::OS {
        "macos" => "mac",
        "linux" | "android" => "lnx",
        _ => "win",
    }
    .to_owned()
}

#[must_use]
pub(crate) fn platform_version() -> String {
    os_info::get().version().to_string()
}

const MAC_CACHE_TTL: Duration = Duration::from_mins(5);

struct CachedMac {
    value: String,
    observed_at: Instant,
}

static MAC_ADDRESS: OnceLock<Mutex<Option<CachedMac>>> = OnceLock::new();

pub(crate) fn mac_address() -> String {
    let cache = MAC_ADDRESS.get_or_init(|| Mutex::new(None));
    let mut cached = mutex(cache);
    if let Some(entry) = cached.as_ref()
        && entry.observed_at.elapsed() < MAC_CACHE_TTL
    {
        return entry.value.clone();
    }
    let value = detected_mac_address();
    *cached = Some(CachedMac {
        value: value.clone(),
        observed_at: Instant::now(),
    });
    value
}

fn detected_mac_address() -> String {
    let bytes = mac_address2::get_mac_address()
        .ok()
        .flatten()
        .map(mac_address2::MacAddress::bytes)
        .filter(|bytes| bytes.iter().any(|byte| *byte != 0))
        .unwrap_or_else(|| {
            let random = UUID::random()
                .unwrap_or_else(|_| UUID::zero())
                .get_bytes()
                .unwrap_or_default();
            random[random.len().saturating_sub(6)..]
                .try_into()
                .unwrap_or([0; 6])
        });
    bytes
        .iter()
        .map(|byte| format!("{byte:02X}"))
        .collect::<Vec<_>>()
        .join(":")
}

pub(crate) fn refresh_mac() {
    *mutex(MAC_ADDRESS.get_or_init(|| Mutex::new(None))) = None;
    let _ = mac_address();
}

pub(crate) fn hashed_mac() -> Result<String, Error> {
    Ok(Utils::md5_string(mac_address())?.to_uppercase())
}

fn redact_uri(uri: &str) -> String {
    let Ok(mut parsed) = reqwest::Url::parse(uri) else {
        return "<invalid URI>".to_owned();
    };
    let _ = parsed.set_username("");
    let _ = parsed.set_password(None);
    parsed.set_query(None);
    parsed.set_fragment(None);
    parsed.to_string()
}

fn osd_string(map: &HashMap<String, OSD>, key: &str) -> Result<String, Error> {
    map.get(key)
        .map(OSD::as_string)
        .transpose()
        .map(Option::unwrap_or_default)
}

fn osd_uint(map: &HashMap<String, OSD>, key: &str) -> Result<u32, Error> {
    map.get(key)
        .map(OSD::as_u_integer)
        .transpose()
        .map(Option::unwrap_or_default)
}

fn optional_uint(map: &HashMap<String, OSD>, key: &str) -> Result<Option<u32>, Error> {
    map.get(key).map(OSD::as_u_integer).transpose()
}

fn osd_uuid(map: &HashMap<String, OSD>, key: &str) -> Result<UUID, Error> {
    map.get(key)
        .map(OSD::as_uuid)
        .transpose()
        .map(|value| value.unwrap_or_else(UUID::zero))
}

fn osd_real_or(map: &HashMap<String, OSD>, key: &str, default: f64) -> Result<f64, Error> {
    map.get(key)
        .map(OSD::as_real)
        .transpose()
        .map(|value| value.unwrap_or(default))
}

fn osd_vector3(map: &HashMap<String, OSD>, key: &str) -> Result<Vector3, Error> {
    let Some(value) = map.get(key) else {
        return Ok(Vector3::zero());
    };
    match value {
        OSD::Array(_) => value.as_vector3(),
        OSD::String(notation) => {
            OSDParser::deserialize_llsd_notation_with_string(notation.clone())?.as_vector3()
        }
        _ => Ok(Vector3::zero()),
    }
}

fn osd_string_array(map: &HashMap<String, OSD>, key: &str) -> Result<Vec<String>, Error> {
    match map.get(key) {
        Some(OSD::Array(values)) => values.iter().map(OSD::as_string).collect(),
        _ => Ok(Vec::new()),
    }
}

fn osd_object_array(map: &HashMap<String, OSD>, key: &str) -> Result<Vec<Object>, Error> {
    match map.get(key) {
        Some(OSD::Array(values)) => values.iter().cloned().map(osd_to_object).collect(),
        _ => Ok(Vec::new()),
    }
}

fn parse_home(map: &HashMap<String, OSD>) -> Result<HomeInfo, Error> {
    let home_map = match map.get("home_info") {
        Some(OSD::Map(map)) => Some(map.clone()),
        _ => match map.get("home") {
            Some(OSD::Map(map)) => Some(map.clone()),
            Some(OSD::String(notation)) => {
                match OSDParser::deserialize_llsd_notation_with_string(notation.clone())? {
                    OSD::Map(map) => Some(map),
                    _ => None,
                }
            }
            _ => None,
        },
    };
    let Some(home_map) = home_map else {
        return Ok(HomeInfo {
            look_at: Vector3::zero(),
            position: Vector3::zero(),
            region_handle: 0,
        });
    };
    let region_handle = match home_map.get("region_handle") {
        Some(OSD::Array(coords)) if coords.len() == 2 => {
            (u64::from(coords[0].as_u_integer()?) << 32) | u64::from(coords[1].as_u_integer()?)
        }
        Some(value) => {
            let notation = value.as_string()?;
            match OSDParser::deserialize_llsd_notation_with_string(notation)? {
                OSD::Array(coords) if coords.len() == 2 => {
                    (u64::from(coords[0].as_u_integer()?) << 32)
                        | u64::from(coords[1].as_u_integer()?)
                }
                _ => 0,
            }
        }
        None => 0,
    };
    Ok(HomeInfo {
        look_at: osd_vector3(&home_map, "look_at")?,
        position: osd_vector3(&home_map, "position")?,
        region_handle,
    })
}

fn parse_buddy_list(map: &HashMap<String, OSD>) -> Result<Vec<BuddyListEntry>, Error> {
    let Some(OSD::Array(values)) = map.get("buddy-list") else {
        return Ok(Vec::new());
    };
    values
        .iter()
        .filter_map(|value| match value {
            OSD::Map(map) => Some(map),
            _ => None,
        })
        .map(|buddy| {
            Ok(BuddyListEntry {
                buddy_id: osd_string(buddy, "buddy_id")?,
                buddy_rights_given: osd_uint(buddy, "buddy_rights_given")?.cast_signed(),
                buddy_rights_has: osd_uint(buddy, "buddy_rights_has")?.cast_signed(),
            })
        })
        .collect()
}

fn parse_mapped_uuid_map(
    key: &str,
    nested_key: &str,
    reply: &HashMap<String, OSD>,
) -> Result<UUID, Error> {
    match reply.get(key) {
        Some(OSD::Array(values)) if values.len() == 1 => match &values[0] {
            OSD::Map(map) => osd_uuid(map, nested_key),
            _ => Ok(UUID::zero()),
        },
        _ => Ok(UUID::zero()),
    }
}

fn parse_inventory_skeleton_map(
    key: &str,
    owner: UUID,
    reply: &HashMap<String, OSD>,
) -> Result<Vec<InventoryFolder>, Error> {
    let Some(OSD::Array(values)) = reply.get(key) else {
        return Ok(Vec::new());
    };
    values
        .iter()
        .filter_map(|value| match value {
            OSD::Map(map) => Some(map),
            _ => None,
        })
        .map(|map| {
            let mut folder = InventoryFolder::new(osd_uuid(map, "folder_id")?)?;
            folder.base.set_name(osd_string(map, "name")?);
            folder.base.set_parent_uuid(osd_uuid(map, "parent_id")?);
            folder.base.set_owner_id(owner);
            let folder_type_value = osd_uint(map, "type_default")?.to_le_bytes()[0].cast_signed();
            folder.set_preferred_type(folder_type(folder_type_value));
            folder.set_version(osd_uint(map, "version")?.cast_signed());
            Ok(folder)
        })
        .collect()
}

fn folder_type(value: i8) -> FolderType {
    match value {
        0 => FolderType::Texture,
        1 => FolderType::Sound,
        2 => FolderType::CallingCard,
        3 => FolderType::Landmark,
        5 => FolderType::Clothing,
        6 => FolderType::Object,
        7 => FolderType::Notecard,
        8 => FolderType::Root,
        9 => FolderType::OldRoot,
        10 => FolderType::LSLText,
        13 => FolderType::BodyPart,
        14 => FolderType::Trash,
        15 => FolderType::Snapshot,
        16 => FolderType::LostAndFound,
        20 => FolderType::Animation,
        21 => FolderType::Gesture,
        23 => FolderType::Favorites,
        26 => FolderType::EnsembleStart,
        45 => FolderType::EnsembleEnd,
        46 => FolderType::CurrentOutfit,
        47 => FolderType::Outfit,
        48 => FolderType::MyOutfits,
        49 => FolderType::Mesh,
        50 => FolderType::Inbox,
        51 => FolderType::Outbox,
        52 => FolderType::BasicRoot,
        53 => FolderType::MarketplaceListings,
        54 => FolderType::MarketplaceStock,
        55 => FolderType::MarketplaceVersion,
        56 => FolderType::Settings,
        57 => FolderType::Material,
        100 => FolderType::Suitcase,
        _ => FolderType::None,
    }
}

fn hashtable_to_map(table: &Hashtable) -> Result<HashMap<String, OSD>, Error> {
    table
        .0
        .iter()
        .filter_map(|(key, value)| match key {
            Object::String(key) => {
                Some(object_to_osd(value.clone()).map(|value| (key.clone(), value)))
            }
            _ => None,
        })
        .collect()
}

fn object_to_osd(value: Object) -> Result<OSD, Error> {
    Ok(match value {
        Object::Boolean(value) => OSD::Boolean(value),
        Object::Integer(value) => OSD::Integer(value),
        Object::UInteger(value) => OSD::Binary(value.to_be_bytes().to_vec()),
        Object::Long(value) => OSD::Binary(value.to_be_bytes().to_vec()),
        Object::ULong(value) => OSD::Binary(value.to_be_bytes().to_vec()),
        Object::Real(value) => OSD::Real(value),
        Object::Bytes(value) => OSD::Binary(value),
        Object::Uri(value) => OSD::Uri(value),
        Object::Array(values) => OSD::Array(
            values
                .into_iter()
                .map(object_to_osd)
                .collect::<Result<_, _>>()?,
        ),
        Object::Map(values) => OSD::Map(
            values
                .into_iter()
                .map(|(key, value)| Ok((key, object_to_osd(value)?)))
                .collect::<Result<_, Error>>()?,
        ),
        Object::String(value) => OSD::String(value),
        Object::UUID(value) => OSD::UUID(value),
        Object::Vector2(value) => OSD::from_vector2(value)?,
        Object::Vector3(value) => OSD::from_vector3(value)?,
        Object::Vector3d(value) => OSD::from_vector3d(value)?,
        Object::Vector4(value) => OSD::from_vector4(value)?,
        Object::Quaternion(value) => OSD::from_quaternion(value)?,
        Object::Color4(value) => OSD::from_color4(value)?,
        Object::Date(value) => OSD::Date(value),
        Object::Undefined | Object::Matrix4(_) | Object::Opaque(_) => OSD::Undefined,
    })
}

fn osd_to_object(value: OSD) -> Result<Object, Error> {
    Ok(match value {
        OSD::Boolean(value) => Object::Boolean(value),
        OSD::Integer(value) => Object::Integer(value),
        OSD::Real(value) => Object::Real(value),
        OSD::String(value) | OSD::LlsdXml(value) => Object::String(value),
        OSD::UUID(value) => Object::UUID(value),
        OSD::Date(value) => Object::Date(value),
        OSD::Uri(value) => Object::Uri(value),
        OSD::Binary(value) => Object::Bytes(value),
        OSD::Array(values) => Object::Array(
            values
                .into_iter()
                .map(osd_to_object)
                .collect::<Result<_, _>>()?,
        ),
        OSD::Map(values) => Object::Map(
            values
                .into_iter()
                .map(|(key, value)| Ok((key, osd_to_object(value)?)))
                .collect::<Result<_, Error>>()?,
        ),
        _ => Object::Undefined,
    })
}

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

    #[test]
    fn credential_and_params_debug_redact_all_secrets() {
        let credential = LoginCredential::new_with_string_string_string_string_string(
            "Ada".into(),
            "Lovelace".into(),
            "plain-secret".into(),
            "123456".into(),
            "saved-mfa-secret".into(),
        )
        .unwrap();
        let rendered = format!("{credential:?}");
        assert!(!rendered.contains("plain-secret"));
        assert!(!rendered.contains("123456"));
        assert!(!rendered.contains("saved-mfa-secret"));

        let mut params = LoginParams::new_with_constructor().unwrap();
        params.uri = "https://user:password@example.test/login?token=cap-secret".into();
        params.password = "plain-secret".into();
        params.token = "123456".into();
        params.mfa_hash = "saved-mfa-secret".into();
        let rendered = format!("{params:?}");
        assert!(!rendered.contains("user:password"));
        assert!(!rendered.contains("cap-secret"));
        assert!(!rendered.contains("plain-secret"));
        assert!(!rendered.contains("123456"));
        assert!(!rendered.contains("saved-mfa-secret"));
    }

    #[test]
    fn response_parser_covers_redirect_home_inventory_and_benefits() {
        let agent = UUID::new_with_string("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa".into()).unwrap();
        let folder = UUID::new_with_string("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb".into()).unwrap();
        let response = LoginResponseData::new().unwrap();
        let map = OSDMap::new_with_dictionary(HashMap::from([
            ("login".into(), OSD::Boolean(true)),
            ("agent_id".into(), OSD::UUID(agent)),
            (
                "home_info".into(),
                OSD::Map(HashMap::from([
                    (
                        "region_handle".into(),
                        OSD::Array(vec![OSD::Integer(1000), OSD::Integer(2000)]),
                    ),
                    (
                        "position".into(),
                        OSD::Array(vec![OSD::Real(128.0), OSD::Real(129.0), OSD::Real(30.0)]),
                    ),
                    (
                        "look_at".into(),
                        OSD::Array(vec![OSD::Real(1.0), OSD::Real(0.0), OSD::Real(0.0)]),
                    ),
                ])),
            ),
            (
                "inventory-skeleton".into(),
                OSD::Array(vec![OSD::Map(HashMap::from([
                    ("folder_id".into(), OSD::UUID(folder)),
                    ("parent_id".into(), OSD::UUID(UUID::zero())),
                    ("name".into(), OSD::String("Objects".into())),
                    ("type_default".into(), OSD::Integer(6)),
                    ("version".into(), OSD::Integer(4)),
                ]))]),
            ),
            (
                "account_level_benefits".into(),
                OSD::Map(HashMap::from([
                    ("texture_upload_cost".into(), OSD::Integer(10)),
                    (
                        "large_texture_upload_cost".into(),
                        OSD::Array(vec![OSD::Integer(30), OSD::Integer(20)]),
                    ),
                    ("attachment_limit".into(), OSD::Integer(42)),
                    (
                        "estate_access_token".into(),
                        OSD::String("estate-secret".into()),
                    ),
                ])),
            ),
        ]))
        .unwrap();
        let result = response.parse_with_osd_map(map).unwrap();
        assert!(result.success());
        assert_eq!(response.home().region_handle, (1000_u64 << 32) | 2000);
        assert_eq!(response.inventory_skeleton().len(), 1);
        let inventory = response.inventory_skeleton();
        assert_eq!(inventory[0].base.uuid(), folder);
        assert_eq!(inventory[0].base.owner_id(), agent);
        assert_eq!(inventory[0].base.name(), "Objects");
        let benefits = response.account_level_benefits().unwrap();
        assert_eq!(benefits.attachment_limit(), 42);
        assert_eq!(benefits.large_texture_upload_cost(), [20, 30]);
        assert!(!format!("{benefits:?}").contains("estate-secret"));
    }

    #[test]
    fn mac_identity_is_cached_and_uses_reference_format() {
        let first = mac_address();
        assert_eq!(first.len(), 17);
        assert_eq!(first.as_bytes()[2], b':');
        assert_eq!(first.as_bytes()[5], b':');
        assert_eq!(first.as_bytes()[8], b':');
        assert_eq!(first.as_bytes()[11], b':');
        assert_eq!(first.as_bytes()[14], b':');
        assert!(
            first
                .bytes()
                .all(|byte| byte == b':' || byte.is_ascii_hexdigit())
        );
        assert_eq!(mac_address(), first);
        assert_eq!(
            hashed_mac().unwrap(),
            Utils::md5_string(first).unwrap().to_uppercase()
        );
        assert!(matches!(platform().as_str(), "mac" | "lnx" | "win"));
        assert!(!platform_version().is_empty());
        refresh_mac();
        assert_eq!(mac_address().len(), 17);
    }

    #[test]
    fn start_locations_match_legacy_reference_encoding() {
        assert_eq!(normalize_start("home").unwrap(), "home");
        assert_eq!(
            normalize_start("secondlife://Hooper/1/2/3/").unwrap(),
            "uri:Hooper&1&2&3"
        );
        assert_eq!(normalize_start("Hooper").unwrap(), "uri:Hooper&128&128&0");
        assert_eq!(
            normalize_start("uri:Hooper&4&5&6").unwrap(),
            "uri:Hooper&4&5&6"
        );
    }
}
