//! Strongly typed decoding for capability event names.

// These public methods mirror generated C# signatures, including Result-returning
// constructors and value-taking parameters that are infallible in native Rust.
#![allow(clippy::inherent_to_string)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::must_use_candidate)]

use crate::interfaces::IMessage;
use crate::message_codec::GeneratedMessage;
use crate::messages::linden::{
    AgentDropGroupMessage, AgentGroupDataUpdateMessage, AgentStateUpdateMessage,
    BulkUpdateInventoryMessage, ChatSessionAcceptInvitation, ChatSessionRequestMuteUpdate,
    ChatSessionRequestStartConference, ChatterBoxInvitationMessage,
    ChatterBoxSessionAgentListUpdatesMessage, ChatterBoxSessionStartReplyMessage,
    ChatterboxSessionEventReplyMessage, CopyInventoryFromNotecardMessage, CrossedRegionMessage,
    DirLandReplyMessage, DisplayNameUpdateMessage, EnableSimulatorMessage,
    EstablishAgentCommunicationMessage, ForceCloseChatterBoxSessionMessage, GetDisplayNamesMessage,
    GetObjectCostMessage, GetObjectCostRequest, LandResourcesMessage, LandResourcesRequest,
    LandStatReplyMessage, MapLayerReplyVariant, NavMeshStatusUpdateMessage,
    ObjectMediaNavigateMessage, ObjectMediaRequest, ObjectMediaResponse, ObjectMediaUpdate,
    ObjectPhysicsPropertiesMessage, ObjectResourcesDetail, ParcelObjectOwnersReplyMessage,
    ParcelPropertiesMessage, ParcelPropertiesUpdateMessage, ParcelVoiceInfoRequestMessage,
    PlacesReplyMessage, ProvisionVoiceAccountRequestMessage, RegionInfoMessage,
    RemoteParcelRequestReply, RenderMaterialsMessage, RequiredVoiceVersionMessage,
    ScriptRunningReplyMessage, SearchStatRequestReply, SearchStatRequestRequest,
    SendPostcardMessage, SetDisplayNameMessage, SetDisplayNameReplyMessage,
    SimConsoleResponseMessage, TeleportFailedMessage, TeleportFinishMessage,
    UpdateAgentInformationMessage, UpdateAgentInventoryRequestMessage, UpdateAgentLanguageMessage,
    UpdateScriptAgentRequestMessage, UpdateScriptTaskUpdateMessage, UploaderRequestComplete,
    UploaderRequestUpload, ViewerStatsMessage,
};
use crate::{Error, MediaControls, MediaPermission};
use libremetaverse_structured_data::{OSD, OSDMap};
use libremetaverse_types::compat::Uri;
use libremetaverse_types::{AttachmentPoint, PhysicsShapeType, UUID};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr};
use std::time::{SystemTime, UNIX_EPOCH};

/// Display-name data carried by the display-name capability messages.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AgentDisplayName {
    id: UUID,
    user_name: Option<String>,
    display_name: Option<String>,
    legacy_first_name: Option<String>,
    legacy_last_name: Option<String>,
    is_default_display_name: bool,
    next_update: SystemTime,
    updated: SystemTime,
}

impl AgentDisplayName {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            id: UUID::zero(),
            user_name: None,
            display_name: None,
            legacy_first_name: None,
            legacy_last_name: None,
            is_default_display_name: false,
            next_update: UNIX_EPOCH,
            updated: UNIX_EPOCH,
        })
    }

    pub fn from_osd(data: OSD) -> Result<Self, Error> {
        let OSD::Map(map) = data else {
            return Err(Error::Argument);
        };
        let mut value = Self::new()?;
        value.id = map.get("id").unwrap_or(&OSD::Undefined).as_uuid()?;
        value.user_name = optional_string(map.get("username"))?;
        value.display_name = optional_string(map.get("display_name"))?;
        value.legacy_first_name = optional_string(map.get("legacy_first_name"))?;
        value.legacy_last_name = optional_string(map.get("legacy_last_name"))?;
        value.is_default_display_name = map
            .get("is_display_name_default")
            .unwrap_or(&OSD::Undefined)
            .as_boolean()?;
        value.next_update = map
            .get("display_name_next_update")
            .unwrap_or(&OSD::Undefined)
            .as_date()?;
        value.updated = map
            .get("last_updated")
            .unwrap_or(&OSD::Undefined)
            .as_date()?;
        Ok(value)
    }

    pub fn get_osd(&self) -> Result<OSD, Error> {
        Ok(OSD::Map(HashMap::from([
            ("id".into(), OSD::UUID(self.id)),
            (
                "username".into(),
                OSD::String(self.user_name.clone().unwrap_or_default()),
            ),
            (
                "display_name".into(),
                OSD::String(self.display_name.clone().unwrap_or_default()),
            ),
            (
                "legacy_first_name".into(),
                OSD::String(self.legacy_first_name.clone().unwrap_or_default()),
            ),
            (
                "legacy_last_name".into(),
                OSD::String(self.legacy_last_name.clone().unwrap_or_default()),
            ),
            (
                "is_display_name_default".into(),
                OSD::Boolean(self.is_default_display_name),
            ),
            (
                "display_name_next_update".into(),
                OSD::Date(self.next_update),
            ),
            ("last_updated".into(), OSD::Date(self.updated)),
        ])))
    }

    pub fn to_string(&self) -> String {
        format!(
            "AgentDisplayName {{ id: {}, user_name: {:?}, display_name: {:?}, legacy_first_name: {:?}, legacy_last_name: {:?}, is_default_display_name: {}, next_update: {:?}, updated: {:?} }}",
            self.id,
            self.user_name,
            self.display_name,
            self.legacy_first_name,
            self.legacy_last_name,
            self.is_default_display_name,
            self.next_update,
            self.updated
        )
    }

    pub fn display_name(&self) -> Option<String> {
        self.display_name.clone()
    }
    pub fn set_display_name(&mut self, value: Option<String>) {
        self.display_name = value;
    }
    pub fn id(&self) -> UUID {
        self.id
    }
    pub fn set_id(&mut self, value: UUID) {
        self.id = value;
    }
    pub fn is_default_display_name(&self) -> bool {
        self.is_default_display_name
    }
    pub fn set_is_default_display_name(&mut self, value: bool) {
        self.is_default_display_name = value;
    }
    pub fn legacy_first_name(&self) -> Option<String> {
        self.legacy_first_name.clone()
    }
    pub fn set_legacy_first_name(&mut self, value: Option<String>) {
        self.legacy_first_name = value;
    }
    pub fn legacy_full_name(&self) -> String {
        format!(
            "{} {}",
            self.legacy_first_name.as_deref().unwrap_or_default(),
            self.legacy_last_name.as_deref().unwrap_or_default()
        )
    }
    pub fn legacy_last_name(&self) -> Option<String> {
        self.legacy_last_name.clone()
    }
    pub fn set_legacy_last_name(&mut self, value: Option<String>) {
        self.legacy_last_name = value;
    }
    pub fn next_update(&self) -> SystemTime {
        self.next_update
    }
    pub fn set_next_update(&mut self, value: SystemTime) {
        self.next_update = value;
    }
    pub fn updated(&self) -> SystemTime {
        self.updated
    }
    pub fn set_updated(&mut self, value: SystemTime) {
        self.updated = value;
    }
    pub fn user_name(&self) -> Option<String> {
        self.user_name.clone()
    }
    pub fn set_user_name(&mut self, value: Option<String>) {
        self.user_name = value;
    }
}

fn optional_string(value: Option<&OSD>) -> Result<Option<String>, Error> {
    match value {
        None | Some(OSD::Undefined) => Ok(None),
        Some(value) => Ok(Some(value.as_string()?)),
    }
}

/// Physics attributes carried by `ObjectPhysicsProperties` events.
#[derive(Clone, Debug, PartialEq)]
pub struct PrimitivePhysicsProperties {
    pub density: f32,
    pub friction: f32,
    pub gravity_multiplier: f32,
    pub local_id: u32,
    pub physics_shape_type: PhysicsShapeType,
    pub restitution: f32,
}

impl PrimitivePhysicsProperties {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            density: 0.0,
            friction: 0.0,
            gravity_multiplier: 0.0,
            local_id: 0,
            physics_shape_type: PhysicsShapeType::Prim,
            restitution: 0.0,
        })
    }

    #[allow(clippy::cast_possible_truncation)] // Matches the C# double-to-float LLSD conversion.
    pub fn from_osd(osd: OSD) -> Result<Self, Error> {
        let OSD::Map(map) = osd else {
            return Self::new();
        };
        let shape = map
            .get("PhysicsShapeType")
            .unwrap_or(&OSD::Undefined)
            .as_integer()?;
        Ok(Self {
            local_id: map
                .get("LocalID")
                .unwrap_or(&OSD::Undefined)
                .as_u_integer()?,
            density: map.get("Density").unwrap_or(&OSD::Undefined).as_real()? as f32,
            friction: map.get("Friction").unwrap_or(&OSD::Undefined).as_real()? as f32,
            gravity_multiplier: map
                .get("GravityMultiplier")
                .unwrap_or(&OSD::Undefined)
                .as_real()? as f32,
            restitution: map
                .get("Restitution")
                .unwrap_or(&OSD::Undefined)
                .as_real()? as f32,
            physics_shape_type: match shape {
                1 => PhysicsShapeType::None,
                2 => PhysicsShapeType::ConvexHull,
                _ => PhysicsShapeType::Prim,
            },
        })
    }

    pub fn get_osd(&self) -> Result<OSD, Error> {
        Ok(OSD::Map(HashMap::from([
            ("LocalID".into(), OSD::from_u_integer(self.local_id)?),
            ("Density".into(), OSD::Real(f64::from(self.density))),
            ("Friction".into(), OSD::Real(f64::from(self.friction))),
            (
                "GravityMultiplier".into(),
                OSD::Real(f64::from(self.gravity_multiplier)),
            ),
            ("Restitution".into(), OSD::Real(f64::from(self.restitution))),
            (
                "PhysicsShapeType".into(),
                OSD::Integer(self.physics_shape_type as i32),
            ),
        ])))
    }
}

/// Per-face media settings used by object-media capability messages.
#[allow(clippy::struct_excessive_bools)] // The flags are independent wire fields in the C# schema.
#[derive(Clone, Debug, PartialEq)]
pub struct MediaEntry {
    undefined: bool,
    enable_alternative_image: bool,
    pub auto_loop: bool,
    pub auto_play: bool,
    pub auto_scale: bool,
    pub auto_zoom: bool,
    pub control_permissions: MediaPermission,
    pub controls: MediaControls,
    pub current_url: String,
    pub enable_white_list: bool,
    pub height: i32,
    pub home_url: String,
    pub interact_on_first_click: bool,
    pub interact_permissions: MediaPermission,
    pub white_list: Vec<String>,
    pub width: i32,
}

impl MediaEntry {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            undefined: false,
            enable_alternative_image: false,
            auto_loop: false,
            auto_play: false,
            auto_scale: false,
            auto_zoom: false,
            control_permissions: MediaPermission::NONE,
            controls: MediaControls::Standard,
            current_url: String::new(),
            enable_white_list: false,
            height: 0,
            home_url: String::new(),
            interact_on_first_click: false,
            interact_permissions: MediaPermission::NONE,
            white_list: Vec::new(),
            width: 0,
        })
    }

    pub fn from_osd(osd: OSD) -> Result<Self, Error> {
        if matches!(osd, OSD::Undefined) {
            let mut entry = Self::new()?;
            entry.undefined = true;
            return Ok(entry);
        }
        let OSD::Map(map) = osd else {
            return Err(Error::Argument);
        };
        let mut entry = Self::new()?;
        entry.enable_alternative_image = osd_bool(&map, "alt_image_enable")?;
        entry.auto_loop = osd_bool(&map, "auto_loop")?;
        entry.auto_play = osd_bool(&map, "auto_play")?;
        entry.auto_scale = osd_bool(&map, "auto_scale")?;
        entry.auto_zoom = osd_bool(&map, "auto_zoom")?;
        entry.controls = match osd_integer(&map, "controls")? {
            1 => MediaControls::Mini,
            _ => MediaControls::Standard,
        };
        entry.current_url = osd_string(&map, "current_url")?;
        entry.interact_on_first_click = osd_bool(&map, "first_click_interact")?;
        entry.height = osd_integer(&map, "height_pixels")?;
        entry.home_url = osd_string(&map, "home_url")?;
        entry.control_permissions = MediaPermission(
            u8::try_from(osd_integer(&map, "perms_control")?).map_err(|_| Error::Argument)?,
        );
        entry.interact_permissions = MediaPermission(
            u8::try_from(osd_integer(&map, "perms_interact")?).map_err(|_| Error::Argument)?,
        );
        if let Some(OSD::Array(values)) = map.get("whitelist") {
            entry.white_list = values
                .iter()
                .map(OSD::as_string)
                .collect::<Result<Vec<_>, _>>()?;
        }
        entry.enable_white_list = osd_bool(&map, "whitelist_enable")?;
        entry.width = osd_integer(&map, "width_pixels")?;
        Ok(entry)
    }

    pub fn get_osd(&self) -> Result<OSDMap, Error> {
        OSDMap::new_with_dictionary(HashMap::from([
            (
                "alt_image_enable".into(),
                OSD::Boolean(self.enable_alternative_image),
            ),
            ("auto_loop".into(), OSD::Boolean(self.auto_loop)),
            ("auto_play".into(), OSD::Boolean(self.auto_play)),
            ("auto_scale".into(), OSD::Boolean(self.auto_scale)),
            ("auto_zoom".into(), OSD::Boolean(self.auto_zoom)),
            ("controls".into(), OSD::Integer(self.controls as i32)),
            ("current_url".into(), OSD::String(self.current_url.clone())),
            (
                "first_click_interact".into(),
                OSD::Boolean(self.interact_on_first_click),
            ),
            ("height_pixels".into(), OSD::Integer(self.height)),
            ("home_url".into(), OSD::String(self.home_url.clone())),
            (
                "perms_control".into(),
                OSD::Integer(i32::from(self.control_permissions.0)),
            ),
            (
                "perms_interact".into(),
                OSD::Integer(i32::from(self.interact_permissions.0)),
            ),
            (
                "whitelist".into(),
                OSD::Array(self.white_list.iter().cloned().map(OSD::String).collect()),
            ),
            (
                "whitelist_enable".into(),
                OSD::Boolean(self.enable_white_list),
            ),
            ("width_pixels".into(), OSD::Integer(self.width)),
        ]))
    }

    pub fn enable_alternative_image(&self) -> bool {
        self.enable_alternative_image
    }

    pub fn set_enable_alternative_image(&mut self, value: bool) {
        self.enable_alternative_image = value;
    }

    pub(crate) const fn is_undefined(&self) -> bool {
        self.undefined
    }
}

fn osd_bool(map: &HashMap<String, OSD>, key: &str) -> Result<bool, Error> {
    map.get(key).unwrap_or(&OSD::Undefined).as_boolean()
}

fn osd_integer(map: &HashMap<String, OSD>, key: &str) -> Result<i32, Error> {
    map.get(key).unwrap_or(&OSD::Undefined).as_integer()
}

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

/// Resource-usage response for the agent's attachments.
pub struct AttachmentResourcesMessage {
    pub attachments: HashMap<AttachmentPoint, Vec<ObjectResourcesDetail>>,
    pub summary_available: HashMap<String, i32>,
    pub summary_used: HashMap<String, i32>,
}

impl AttachmentResourcesMessage {
    pub fn new() -> Result<Self, Error> {
        Ok(Self {
            attachments: HashMap::new(),
            summary_available: HashMap::new(),
            summary_used: HashMap::new(),
        })
    }

    #[allow(clippy::needless_pass_by_value)] // Preserves the generated C# API mapping.
    pub fn deserialize(&mut self, osd: OSDMap) -> Result<(), Error> {
        self.summary_available.clear();
        self.summary_used.clear();
        let summary = osd.get("summary").ok_or(Error::Argument)?;
        let OSD::Map(summary) = summary else {
            return Err(Error::Argument);
        };
        decode_resource_summary(summary.get("available"), &mut self.summary_available)?;
        decode_resource_summary(summary.get("used"), &mut self.summary_used)?;

        self.attachments.clear();
        let attachments = osd.get("attachments").ok_or(Error::Argument)?;
        let OSD::Array(attachments) = attachments else {
            return Err(Error::Argument);
        };
        for attachment in attachments {
            let OSD::Map(attachment) = attachment else {
                return Err(Error::Argument);
            };
            let location = attachment
                .get("location")
                .unwrap_or(&OSD::Undefined)
                .as_string()?;
            let point = attachment_point_from_string(&location);
            let Some(OSD::Array(objects)) = attachment.get("objects") else {
                return Err(Error::Argument);
            };
            let mut decoded = Vec::with_capacity(objects.len());
            for object in objects {
                decoded.push(decode_object_resources(object)?);
            }
            self.attachments.insert(point, decoded);
        }
        Ok(())
    }

    pub fn from_osd(osd: OSD) -> Result<Self, Error> {
        let OSD::Map(values) = osd else {
            return Err(Error::Argument);
        };
        let mut message = Self::new()?;
        message.deserialize(OSDMap::new_with_dictionary(values)?)?;
        Ok(message)
    }

    pub fn get_message_handler(_map: OSDMap) -> Result<Option<Box<dyn IMessage>>, Error> {
        Ok(Some(Box::new(Self::new()?)))
    }
}

impl IMessage for AttachmentResourcesMessage {
    fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
        Self::deserialize(self, map)
    }

    fn serialize(&self) -> Result<OSDMap, Error> {
        let summary = HashMap::from([
            (
                "available".into(),
                encode_resource_summary(&self.summary_available),
            ),
            ("used".into(), encode_resource_summary(&self.summary_used)),
        ]);
        OSDMap::new_with_dictionary(HashMap::from([("summary".into(), OSD::Map(summary))]))
    }
}

fn decode_resource_summary(
    value: Option<&OSD>,
    target: &mut HashMap<String, i32>,
) -> Result<(), Error> {
    let Some(OSD::Array(values)) = value else {
        return Err(Error::Argument);
    };
    for value in values {
        let OSD::Map(value) = value else {
            return Err(Error::Argument);
        };
        target.insert(osd_string(value, "type")?, osd_integer(value, "amount")?);
    }
    Ok(())
}

fn encode_resource_summary(values: &HashMap<String, i32>) -> OSD {
    let mut entries: Vec<_> = values.iter().collect();
    entries.sort_unstable_by(|left, right| left.0.cmp(right.0));
    OSD::Array(
        entries
            .into_iter()
            .map(|(name, amount)| {
                OSD::Map(HashMap::from([
                    ("type".into(), OSD::String(name.clone())),
                    ("amount".into(), OSD::Integer(*amount)),
                ]))
            })
            .collect(),
    )
}

fn decode_object_resources(value: &OSD) -> Result<ObjectResourcesDetail, Error> {
    let OSD::Map(map) = value else {
        return Err(Error::Argument);
    };
    let resources = match map.get("resources") {
        Some(OSD::Map(values)) => values
            .iter()
            .map(|(key, value)| Ok((key.clone(), value.as_integer()?)))
            .collect::<Result<HashMap<_, _>, Error>>()?,
        _ => return Err(Error::Argument),
    };
    Ok(ObjectResourcesDetail {
        group_owned: osd_bool(map, "is_group_owned")?,
        id: map.get("id").unwrap_or(&OSD::Undefined).as_uuid()?,
        location: map
            .get("location")
            .unwrap_or(&OSD::Undefined)
            .as_vector3d()?,
        name: osd_string(map, "name")?,
        owner_id: map.get("owner_id").unwrap_or(&OSD::Undefined).as_uuid()?,
        resources,
    })
}

fn attachment_point_from_string(value: &str) -> AttachmentPoint {
    const POINTS: &[AttachmentPoint] = &[
        AttachmentPoint::AltLeftEar,
        AttachmentPoint::AltLeftEye,
        AttachmentPoint::AltRightEar,
        AttachmentPoint::AltRightEye,
        AttachmentPoint::Chest,
        AttachmentPoint::Chin,
        AttachmentPoint::Default,
        AttachmentPoint::Groin,
        AttachmentPoint::HUDBottom,
        AttachmentPoint::HUDBottomLeft,
        AttachmentPoint::HUDBottomRight,
        AttachmentPoint::HUDCenter,
        AttachmentPoint::HUDCenter2,
        AttachmentPoint::HUDTop,
        AttachmentPoint::HUDTopLeft,
        AttachmentPoint::HUDTopRight,
        AttachmentPoint::Jaw,
        AttachmentPoint::LeftEar,
        AttachmentPoint::LeftEyeball,
        AttachmentPoint::LeftFoot,
        AttachmentPoint::LeftForearm,
        AttachmentPoint::LeftHand,
        AttachmentPoint::LeftHandRing,
        AttachmentPoint::LeftHindFoot,
        AttachmentPoint::LeftHip,
        AttachmentPoint::LeftLowerLeg,
        AttachmentPoint::LeftPec,
        AttachmentPoint::LeftShoulder,
        AttachmentPoint::LeftUpperArm,
        AttachmentPoint::LeftUpperLeg,
        AttachmentPoint::LeftWing,
        AttachmentPoint::Mouth,
        AttachmentPoint::Neck,
        AttachmentPoint::Nose,
        AttachmentPoint::Pelvis,
        AttachmentPoint::RightEar,
        AttachmentPoint::RightEyeball,
        AttachmentPoint::RightFoot,
        AttachmentPoint::RightForearm,
        AttachmentPoint::RightHand,
        AttachmentPoint::RightHandRing,
        AttachmentPoint::RightHindFoot,
        AttachmentPoint::RightHip,
        AttachmentPoint::RightLowerLeg,
        AttachmentPoint::RightPec,
        AttachmentPoint::RightShoulder,
        AttachmentPoint::RightUpperArm,
        AttachmentPoint::RightUpperLeg,
        AttachmentPoint::RightWing,
        AttachmentPoint::Root,
        AttachmentPoint::Skull,
        AttachmentPoint::Spine,
        AttachmentPoint::Stomach,
        AttachmentPoint::TailBase,
        AttachmentPoint::TailTip,
        AttachmentPoint::Tongue,
    ];
    let normalized: String = value
        .chars()
        .filter(char::is_ascii_alphanumeric)
        .flat_map(char::to_lowercase)
        .collect();
    POINTS
        .iter()
        .copied()
        .find(|point| format!("{point:?}").to_ascii_lowercase() == normalized)
        .unwrap_or(AttachmentPoint::Default)
}

fn generated<T>(map: &OSDMap) -> Option<Box<dyn IMessage>>
where
    T: GeneratedMessage + IMessage + 'static,
{
    let mut message = T::new_generated();
    T::deserialize_generated(&mut message, map).ok()?;
    Some(Box::new(message))
}

macro_rules! message_variant {
    ($type:ty) => {
        impl IMessage for $type {
            fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
                <Self as GeneratedMessage>::deserialize_generated(self, &map)
            }

            fn serialize(&self) -> Result<OSDMap, Error> {
                <Self as GeneratedMessage>::serialize_generated(self)
            }
        }
    };
}

// These classes are polymorphic blocks in C#. The generated Rust surface keeps
// each concrete variant as a value type, so allowing it to cross the IMessage
// boundary preserves all decoded fields without an allocation-only wrapper.
message_variant!(ChatSessionAcceptInvitation);
message_variant!(ChatSessionRequestMuteUpdate);
message_variant!(ChatSessionRequestStartConference);
message_variant!(MapLayerReplyVariant);
message_variant!(ObjectMediaRequest);
message_variant!(ObjectMediaResponse);
message_variant!(ObjectMediaUpdate);
message_variant!(RemoteParcelRequestReply);
message_variant!(SearchStatRequestReply);
message_variant!(SearchStatRequestRequest);
message_variant!(UpdateAgentInventoryRequestMessage);
message_variant!(UpdateScriptAgentRequestMessage);
message_variant!(UpdateScriptTaskUpdateMessage);
message_variant!(UploaderRequestComplete);
message_variant!(UploaderRequestUpload);

/// Decodes the event names handled by the C# `MessageUtils.DecodeEvent`
/// switch. A malformed known message follows the reference behavior and is
/// returned as `None`, allowing the caller to try packet translation next.
#[allow(clippy::too_many_lines)] // Mirrors the reference decoder's explicit event-name switch.
pub(crate) fn decode_event(
    event_name: &str,
    map: &OSDMap,
) -> Result<Option<Box<dyn IMessage>>, Error> {
    let decoded = match event_name {
        "AgentGroupDataUpdate" | "AvatarGroupsReply" => {
            generated::<AgentGroupDataUpdateMessage>(map)
        }
        "ParcelProperties" => generated::<ParcelPropertiesMessage>(map),
        "ParcelObjectOwnersReply" => generated::<ParcelObjectOwnersReplyMessage>(map),
        "TeleportFinish" => generated::<TeleportFinishMessage>(map),
        "EnableSimulator" => generated::<EnableSimulatorMessage>(map),
        "ParcelPropertiesUpdate" => generated::<ParcelPropertiesUpdateMessage>(map),
        "EstablishAgentCommunication" => generated::<EstablishAgentCommunicationMessage>(map),
        "ChatterBoxInvitation" => generated::<ChatterBoxInvitationMessage>(map),
        "ChatterBoxSessionEventReply" => generated::<ChatterboxSessionEventReplyMessage>(map),
        "ChatterBoxSessionStartReply" => generated::<ChatterBoxSessionStartReplyMessage>(map),
        "ChatterBoxSessionAgentListUpdates" => {
            generated::<ChatterBoxSessionAgentListUpdatesMessage>(map)
        }
        "RequiredVoiceVersion" => generated::<RequiredVoiceVersionMessage>(map),
        "MapLayer" if map.get("LayerData").is_some() => generated::<MapLayerReplyVariant>(map),
        "ChatSessionRequest" => match map
            .get("method")
            .unwrap_or_default()
            .as_string()
            .unwrap_or_default()
            .as_str()
        {
            "start conference" => generated::<ChatSessionRequestStartConference>(map),
            "mute update" => generated::<ChatSessionRequestMuteUpdate>(map),
            "accept invitation" => generated::<ChatSessionAcceptInvitation>(map),
            _ => None,
        },
        "CopyInventoryFromNotecard" => generated::<CopyInventoryFromNotecardMessage>(map),
        "ProvisionVoiceAccountRequest" => generated::<ProvisionVoiceAccountRequestMessage>(map),
        "Viewerstats" | "ViewerStats" => generated::<ViewerStatsMessage>(map),
        "UpdateAgentLanguage" => generated::<UpdateAgentLanguageMessage>(map),
        "RemoteParcelRequest" if map.get("parcel_id").is_some() => {
            generated::<RemoteParcelRequestReply>(map)
        }
        "UpdateScriptTask" if map.get("task_id").is_some() => {
            generated::<UpdateScriptTaskUpdateMessage>(map)
        }
        "UpdateScriptAgent" if map.get("item_id").is_some() => {
            generated::<UpdateScriptAgentRequestMessage>(map)
        }
        "UpdateGestureAgentInventory" | "UpdateNotecardAgentInventory"
            if map.get("item_id").is_some() =>
        {
            generated::<UpdateAgentInventoryRequestMessage>(map)
        }
        "SendPostcard" => generated::<SendPostcardMessage>(map),
        "LandStatReply" => generated::<LandStatReplyMessage>(map),
        "ParcelVoiceInfoRequest" => generated::<ParcelVoiceInfoRequestMessage>(map),
        "EventQueueGet" => {
            let mut message = crate::event_queue::EventQueueGetMessage::new()?;
            match message.deserialize(map.clone()) {
                Ok(()) => Some(Box::new(message) as Box<dyn IMessage>),
                Err(_) => None,
            }
        }
        "CrossedRegion" => generated::<CrossedRegionMessage>(map),
        "SimConsoleResponse" => generated::<SimConsoleResponseMessage>(map),
        "TeleportFailed" => generated::<TeleportFailedMessage>(map),
        "PlacesReply" => generated::<PlacesReplyMessage>(map),
        "UpdateAgentInformation" => generated::<UpdateAgentInformationMessage>(map),
        "DirLandReply" => generated::<DirLandReplyMessage>(map),
        "ScriptRunningReply" => generated::<ScriptRunningReplyMessage>(map),
        "AgentDropGroup" => generated::<AgentDropGroupMessage>(map),
        "AgentStateUpdate" => generated::<AgentStateUpdateMessage>(map),
        "NavMeshStatusUpdate" => generated::<NavMeshStatusUpdateMessage>(map),
        "ForceCloseChatterBoxSession" => generated::<ForceCloseChatterBoxSessionMessage>(map),
        "RegionInfo" => generated::<RegionInfoMessage>(map),
        "ObjectMediaNavigate" => generated::<ObjectMediaNavigateMessage>(map),
        "ObjectMedia" => match map
            .get("verb")
            .unwrap_or_default()
            .as_string()
            .unwrap_or_default()
            .as_str()
        {
            "GET" => generated::<ObjectMediaRequest>(map),
            "UPDATE" => generated::<ObjectMediaUpdate>(map),
            _ if map.get("object_media_version").is_some() => generated::<ObjectMediaResponse>(map),
            _ => None,
        },
        "SetDisplayName" => generated::<SetDisplayNameMessage>(map),
        "SetDisplayNameReply" => generated::<SetDisplayNameReplyMessage>(map),
        "DisplayNameUpdate" => generated::<DisplayNameUpdateMessage>(map),
        "GetDisplayNames" => generated::<GetDisplayNamesMessage>(map),
        "ObjectPhysicsProperties" => generated::<ObjectPhysicsPropertiesMessage>(map),
        "RenderMaterials" => generated::<RenderMaterialsMessage>(map),
        "GetObjectCost" => {
            if map.get("object_ids").is_some() {
                generated::<GetObjectCostRequest>(map)
            } else {
                generated::<GetObjectCostMessage>(map)
            }
        }
        "LandResources" => {
            if map.get("parcel_id").is_some() {
                generated::<LandResourcesRequest>(map)
            } else if map.get("ScriptResourceSummary").is_some() {
                generated::<LandResourcesMessage>(map)
            } else {
                None
            }
        }
        "SearchStatRequest" => {
            if map.get("map_clicks").is_some() {
                generated::<SearchStatRequestReply>(map)
            } else if map.get("classified_id").is_some() {
                generated::<SearchStatRequestRequest>(map)
            } else {
                None
            }
        }
        "UploadBakedTexture" => match map
            .get("state")
            .unwrap_or_default()
            .as_string()
            .unwrap_or_default()
            .as_str()
        {
            "upload" => generated::<UploaderRequestUpload>(map),
            "complete" => generated::<UploaderRequestComplete>(map),
            _ => None,
        },
        "AttachmentResources" => {
            let mut message = AttachmentResourcesMessage::new()?;
            match message.deserialize(map.clone()) {
                Ok(()) => Some(Box::new(message) as Box<dyn IMessage>),
                Err(_) => None,
            }
        }
        "BulkUpdateInventory" => generated::<BulkUpdateInventoryMessage>(map),
        _ => None,
    };
    Ok(decoded)
}

#[allow(clippy::needless_pass_by_value)] // Preserves MessageUtils.ToIP's generated signature.
pub(crate) fn to_ip(osd: OSD) -> Result<IpAddr, Error> {
    let bytes = osd.as_binary()?;
    Ok(<[u8; 4]>::try_from(bytes)
        .map(Ipv4Addr::from)
        .map_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED), IpAddr::V4))
}

#[allow(clippy::unnecessary_wraps)] // Preserves MessageUtils.FromIP's generated signature.
pub(crate) fn from_ip(address: IpAddr) -> Result<OSD, Error> {
    match address {
        IpAddr::V4(address) if !address.is_unspecified() => {
            Ok(OSD::Binary(address.octets().to_vec()))
        }
        IpAddr::V6(address) if !address.is_unspecified() => {
            Ok(OSD::Binary(address.octets().to_vec()))
        }
        IpAddr::V4(_) | IpAddr::V6(_) => Ok(OSD::Undefined),
    }
}

pub(crate) fn to_dictionary_string(osd: OSD) -> Result<HashMap<String, String>, Error> {
    let OSD::Map(map) = osd else {
        return Ok(HashMap::new());
    };
    map.into_iter()
        .map(|(key, value)| Ok((key, value.as_string()?)))
        .collect()
}

pub(crate) fn to_dictionary_uri(osd: OSD) -> Result<HashMap<Uri, Uri>, Error> {
    let OSD::Map(map) = osd else {
        return Ok(HashMap::new());
    };
    let mut dictionary = HashMap::with_capacity(map.len());
    for (key, value) in map {
        if let Some(value) = value.as_uri()? {
            dictionary.insert(Uri(key), value);
        }
    }
    Ok(dictionary)
}

pub(crate) fn from_dictionary_string(dict: HashMap<String, String>) -> Result<OSDMap, Error> {
    OSDMap::new_with_dictionary(
        dict.into_iter()
            .map(|(key, value)| (key, OSD::String(value)))
            .collect(),
    )
}

pub(crate) fn from_dictionary_uri(dict: HashMap<Uri, Uri>) -> Result<OSDMap, Error> {
    OSDMap::new_with_dictionary(
        dict.into_iter()
            .map(|(key, value)| (key.0, OSD::Uri(value)))
            .collect(),
    )
}

#[cfg(test)]
#[allow(clippy::too_many_lines)] // The exhaustive decoder matrix stays in one test.
mod tests {
    use super::*;
    use flate2::Compression;
    use flate2::write::ZlibEncoder;
    use libremetaverse_structured_data::OSDParser;
    use libremetaverse_types::UUID;
    use std::io::Write as _;

    fn map(entries: impl IntoIterator<Item = (&'static str, OSD)>) -> OSDMap {
        OSDMap::new_with_dictionary(
            entries
                .into_iter()
                .map(|(key, value)| (key.to_owned(), value))
                .collect(),
        )
        .unwrap()
    }

    #[test]
    fn known_event_and_alias_decode_to_typed_messages_with_working_trait_codec() {
        let language = map([
            ("language", OSD::String("en-US".to_owned())),
            ("language_is_public", OSD::Boolean(true)),
        ]);
        let decoded = decode_event("UpdateAgentLanguage", &language)
            .unwrap()
            .expect("known message");
        let typed = (decoded.as_ref() as &dyn std::any::Any)
            .downcast_ref::<UpdateAgentLanguageMessage>()
            .expect("typed language message");
        assert_eq!(typed.language, "en-US");
        assert!(typed.language_public);
        assert_eq!(
            decoded.serialize().unwrap().get("language"),
            language.get("language")
        );

        let group = map([
            (
                "AgentData",
                OSD::Array(vec![OSD::Map(HashMap::from([(
                    "AgentID".to_owned(),
                    OSD::UUID(UUID::zero()),
                )]))]),
            ),
            ("GroupData", OSD::Array(Vec::new())),
            ("NewGroupData", OSD::Array(Vec::new())),
        ]);
        let alias = decode_event("AvatarGroupsReply", &group).unwrap();
        assert!(alias.is_some());
    }

    #[test]
    fn dynamic_reference_messages_select_the_matching_concrete_variant() {
        let parcel_id = UUID::random().unwrap();
        let remote = map([("parcel_id", OSD::UUID(parcel_id))]);
        let decoded = decode_event("RemoteParcelRequest", &remote)
            .unwrap()
            .expect("remote reply");
        let typed = (decoded.as_ref() as &dyn std::any::Any)
            .downcast_ref::<RemoteParcelRequestReply>()
            .expect("reply variant");
        assert_eq!(typed.parcel_id, parcel_id);

        let chat = map([
            ("method", OSD::String("accept invitation".to_owned())),
            ("session-id", OSD::UUID(UUID::random().unwrap())),
        ]);
        let decoded = decode_event("ChatSessionRequest", &chat)
            .unwrap()
            .expect("chat variant");
        assert!(
            (decoded.as_ref() as &dyn std::any::Any)
                .downcast_ref::<ChatSessionAcceptInvitation>()
                .is_some()
        );
    }

    #[test]
    fn unknown_or_malformed_schema_returns_none_for_packet_fallback() {
        let empty = OSDMap::new_with_constructor().unwrap();
        assert!(decode_event("FutureGridEvent", &empty).unwrap().is_none());
        assert!(decode_event("ObjectMedia", &empty).unwrap().is_none());
    }

    #[test]
    fn every_reference_decoder_case_has_a_native_typed_path() {
        let display_name = OSD::Map(HashMap::from([
            ("id".into(), OSD::UUID(UUID::zero())),
            ("username".into(), OSD::String("resident".into())),
            ("display_name".into(), OSD::String("Resident".into())),
            ("legacy_first_name".into(), OSD::String("Test".into())),
            ("legacy_last_name".into(), OSD::String("Resident".into())),
            ("is_display_name_default".into(), OSD::Boolean(false)),
            ("display_name_next_update".into(), OSD::Date(UNIX_EPOCH)),
            ("last_updated".into(), OSD::Date(UNIX_EPOCH)),
        ]));
        let cases = [
            (
                "SearchStatRequest",
                map([("classified_id", OSD::UUID(UUID::zero()))]),
            ),
            (
                "UploadBakedTexture",
                map([
                    ("state", OSD::String("complete".into())),
                    ("new_asset", OSD::UUID(UUID::zero())),
                ]),
            ),
            (
                "ObjectMedia",
                map([
                    ("object_id", OSD::UUID(UUID::zero())),
                    ("verb", OSD::String("GET".into())),
                ]),
            ),
            (
                "AttachmentResources",
                map([
                    (
                        "summary",
                        OSD::Map(HashMap::from([
                            ("available".into(), OSD::Array(Vec::new())),
                            ("used".into(), OSD::Array(Vec::new())),
                        ])),
                    ),
                    ("attachments", OSD::Array(Vec::new())),
                ]),
            ),
            (
                "LandResources",
                map([("parcel_id", OSD::UUID(UUID::zero()))]),
            ),
            (
                "GetDisplayNames",
                map([
                    ("agents", OSD::Array(vec![display_name.clone()])),
                    ("bad_ids", OSD::Array(Vec::new())),
                ]),
            ),
            (
                "SetDisplayNameReply",
                map([
                    ("content", display_name.clone()),
                    ("reason", OSD::String("ok".into())),
                    ("status", OSD::Integer(200)),
                ]),
            ),
            (
                "DisplayNameUpdate",
                map([(
                    "agent",
                    match display_name.clone() {
                        OSD::Map(mut agent) => {
                            agent.insert("old_display_name".into(), OSD::String("Old".into()));
                            OSD::Map(agent)
                        }
                        _ => unreachable!(),
                    },
                )]),
            ),
            (
                "ObjectPhysicsProperties",
                map([(
                    "ObjectData",
                    OSD::Array(vec![OSD::Map(HashMap::from([
                        ("LocalID".into(), OSD::Integer(7)),
                        ("Density".into(), OSD::Real(1000.0)),
                        ("Friction".into(), OSD::Real(0.5)),
                        ("GravityMultiplier".into(), OSD::Real(1.0)),
                        ("Restitution".into(), OSD::Real(0.2)),
                        ("PhysicsShapeType".into(), OSD::Integer(2)),
                    ]))]),
                )]),
            ),
            (
                "BulkUpdateInventory",
                map([
                    (
                        "AgentData",
                        OSD::Array(vec![OSD::Map(HashMap::from([
                            ("AgentID".into(), OSD::UUID(UUID::zero())),
                            ("TransactionID".into(), OSD::UUID(UUID::zero())),
                        ]))]),
                    ),
                    ("FolderData", OSD::Array(Vec::new())),
                    ("ItemData", OSD::Array(Vec::new())),
                ]),
            ),
            (
                "GetObjectCost",
                map([("object_ids", OSD::Array(vec![OSD::UUID(UUID::zero())]))]),
            ),
        ];

        for (name, fixture) in cases {
            let decoded = decode_event(name, &fixture)
                .unwrap()
                .unwrap_or_else(|| panic!("{name} must decode"));
            decoded.serialize().expect("typed codec must serialize");
        }
    }

    #[test]
    fn object_media_preserves_null_face_slots_from_the_reference_shape() {
        let fixture = map([
            ("object_id", OSD::UUID(UUID::zero())),
            (
                "object_media_data",
                OSD::Array(vec![
                    OSD::Undefined,
                    OSD::Map(HashMap::from([
                        ("alt_image_enable".into(), OSD::Boolean(false)),
                        ("auto_loop".into(), OSD::Boolean(false)),
                        ("auto_play".into(), OSD::Boolean(true)),
                        ("auto_scale".into(), OSD::Boolean(false)),
                        ("auto_zoom".into(), OSD::Boolean(false)),
                        ("controls".into(), OSD::Integer(0)),
                        (
                            "current_url".into(),
                            OSD::String("https://example.test/".into()),
                        ),
                        ("first_click_interact".into(), OSD::Boolean(true)),
                        ("height_pixels".into(), OSD::Integer(512)),
                        (
                            "home_url".into(),
                            OSD::String("https://example.test/".into()),
                        ),
                        ("perms_control".into(), OSD::Integer(7)),
                        ("perms_interact".into(), OSD::Integer(7)),
                        ("whitelist".into(), OSD::Array(Vec::new())),
                        ("whitelist_enable".into(), OSD::Boolean(false)),
                        ("width_pixels".into(), OSD::Integer(1024)),
                    ])),
                ]),
            ),
            ("object_media_version", OSD::String("x-mv:1/0".into())),
        ]);
        let decoded = decode_event("ObjectMedia", &fixture)
            .unwrap()
            .expect("object media response");
        let encoded = decoded.serialize().unwrap();
        assert!(matches!(
            encoded.get("object_media_data"),
            Some(OSD::Array(values)) if matches!(values.first(), Some(OSD::Undefined))
        ));
    }

    #[test]
    fn remaining_native_reference_switch_cases_decode_without_stubs() {
        let simple_cases = [
            (
                "SimConsoleResponse",
                map([("body", OSD::String("ok".into()))]),
            ),
            (
                "ForceCloseChatterBoxSession",
                map([
                    ("reason", OSD::String("closed".into())),
                    ("session_id", OSD::UUID(UUID::zero())),
                ]),
            ),
            (
                "NavMeshStatusUpdate",
                map([
                    ("region_id", OSD::UUID(UUID::zero())),
                    ("status", OSD::String("complete".into())),
                    ("version", OSD::Integer(4)),
                ]),
            ),
            (
                "ObjectMediaNavigate",
                map([
                    ("current_url", OSD::String("https://example.test/".into())),
                    ("object_id", OSD::UUID(UUID::zero())),
                    ("texture_index", OSD::Integer(2)),
                ]),
            ),
            (
                "RegionInfo",
                map([
                    ("parcel_local_id", OSD::Integer(7)),
                    ("region_name", OSD::String("Region".into())),
                    (
                        "voice_credentials",
                        OSD::Map(HashMap::from([(
                            "channel_uri".to_owned(),
                            OSD::String("sip:region@example.test".into()),
                        )])),
                    ),
                ]),
            ),
            (
                "SetDisplayName",
                map([(
                    "display_name",
                    OSD::Array(vec![OSD::String("Old".into()), OSD::String("New".into())]),
                )]),
            ),
            (
                "AgentDropGroup",
                map([(
                    "AgentData",
                    OSD::Array(vec![OSD::Map(HashMap::from([
                        ("AgentID".to_owned(), OSD::UUID(UUID::zero())),
                        ("GroupID".to_owned(), OSD::UUID(UUID::zero())),
                    ]))]),
                )]),
            ),
        ];
        for (name, fixture) in simple_cases {
            assert!(
                decode_event(name, &fixture).unwrap().is_some(),
                "{name} must decode"
            );
        }

        let object_id = UUID::random().unwrap();
        let costs = OSDMap::new_with_dictionary(HashMap::from([(
            object_id.to_string(),
            OSD::Map(HashMap::from([
                ("linked_set_resource_cost".into(), OSD::Real(1.0)),
                ("resource_cost".into(), OSD::Real(2.0)),
                ("physics_cost".into(), OSD::Real(3.0)),
                ("linked_set_physics_cost".into(), OSD::Real(4.0)),
            ])),
        )]))
        .unwrap();
        assert!(decode_event("GetObjectCost", &costs).unwrap().is_some());

        let binary = OSDParser::serialize_llsd_binary_with_osd(OSD::Map(HashMap::from([(
            "material".to_owned(),
            OSD::String("native".to_owned()),
        )])))
        .unwrap();
        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
        encoder.write_all(&binary).unwrap();
        let render = map([("Zipped", OSD::Binary(encoder.finish().unwrap()))]);
        let decoded = decode_event("RenderMaterials", &render)
            .unwrap()
            .expect("render materials");
        let typed = (decoded.as_ref() as &dyn std::any::Any)
            .downcast_ref::<RenderMaterialsMessage>()
            .unwrap();
        assert!(
            matches!(&typed.material_data, OSD::Map(values) if values.get("material") == Some(&OSD::String("native".into())))
        );
    }

    #[test]
    fn message_utils_ip_and_dictionary_conversions_match_reference_shapes() {
        assert_eq!(
            to_ip(OSD::Binary(vec![127, 0, 0, 1])).unwrap(),
            "127.0.0.1".parse::<IpAddr>().unwrap()
        );
        assert_eq!(
            to_ip(OSD::Binary(vec![1, 2, 3])).unwrap(),
            IpAddr::V4(Ipv4Addr::UNSPECIFIED)
        );
        assert_eq!(
            from_ip("192.0.2.1".parse().unwrap()).unwrap(),
            OSD::Binary(vec![192, 0, 2, 1])
        );
        assert_eq!(
            from_ip(IpAddr::V4(Ipv4Addr::UNSPECIFIED)).unwrap(),
            OSD::Undefined
        );

        let strings = HashMap::from([
            ("a".to_owned(), "one".to_owned()),
            ("b".to_owned(), "two".to_owned()),
        ]);
        let map = from_dictionary_string(strings.clone()).unwrap();
        assert_eq!(
            to_dictionary_string(OSD::Map(map.snapshot())).unwrap(),
            strings
        );

        let uris = HashMap::from([(
            Uri("https://key.example.test/".to_owned()),
            Uri("https://value.example.test/".to_owned()),
        )]);
        let map = from_dictionary_uri(uris.clone()).unwrap();
        assert_eq!(to_dictionary_uri(OSD::Map(map.snapshot())).unwrap(), uris);
        assert!(to_dictionary_string(OSD::Integer(1)).unwrap().is_empty());
    }
}
