//! Recording callbacks shared by the RLV translations.

use libremetaverse_rlv::{
    AttachmentRequest, CameraSettings, IRlvActionCallbacks, IRlvQueryCallbacks, InventoryMap,
    RlvAttachmentPoint, RlvInventoryItem, RlvService, RlvSharedFolder,
};
use libremetaverse_types::Error;
use libremetaverse_types::compat::{CancellationToken, Guid};
use std::collections::{BTreeMap, HashMap};
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};

pub const SENDER_NAME: &str = "Sender 1";
pub const SENDER_ID: &str = "ffffffff-ffff-4fff-8fff-ffffffffffff";

/// Parses a UUID literal used by the pinned RLV fixtures.
///
/// # Panics
///
/// Panics when `value` is not a 32-digit hexadecimal UUID, ignoring hyphens.
#[must_use]
pub fn guid(value: &str) -> Guid {
    let digits: Vec<_> = value.bytes().filter(|byte| *byte != b'-').collect();
    assert_eq!(digits.len(), 32, "valid RLV test UUID");
    let mut bytes = [0; 16];
    for (index, pair) in digits.chunks_exact(2).enumerate() {
        let digit = |byte| match byte {
            b'0'..=b'9' => byte - b'0',
            b'a'..=b'f' => byte - b'a' + 10,
            b'A'..=b'F' => byte - b'A' + 10,
            _ => panic!("valid RLV test UUID"),
        };
        bytes[index] = digit(pair[0]) << 4 | digit(pair[1]);
    }
    Guid(bytes)
}

#[derive(Clone, Debug, PartialEq)]
pub struct AttachmentCall {
    pub item_id: Guid,
    pub point: RlvAttachmentPoint,
    pub replace: bool,
}

#[derive(Clone, Debug, PartialEq)]
pub struct TeleportCall {
    pub x: f32,
    pub y: f32,
    pub z: f32,
    pub region: Option<String>,
    pub look_at: Option<Option<f32>>,
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct ActionLog {
    pub adjusted_heights: Vec<(f32, f32, f32)>,
    pub attachments: Vec<Vec<AttachmentCall>>,
    pub detachments: Vec<Vec<Guid>>,
    pub removed_outfits: Vec<Vec<Guid>>,
    pub instant_messages: Vec<(Guid, String)>,
    pub replies: Vec<(i32, String)>,
    pub camera_fovs: Vec<f32>,
    pub debug_settings: Vec<(String, String)>,
    pub environment_settings: Vec<(String, String)>,
    pub group_ids: Vec<(Guid, Option<String>)>,
    pub group_names: Vec<(String, Option<String>)>,
    pub rotations: Vec<f32>,
    pub sits: Vec<Guid>,
    pub sit_ground_count: usize,
    pub teleports: Vec<TeleportCall>,
    pub unsit_count: usize,
}

#[derive(Clone, Default)]
pub struct RecordingActions(pub Arc<Mutex<ActionLog>>);

type ActionFuture<'a> = Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>>;

impl IRlvActionCallbacks for RecordingActions {
    fn adjust_height(
        &self,
        distance: f32,
        factor: f32,
        delta: f32,
        _: CancellationToken,
    ) -> ActionFuture<'_> {
        self.0
            .lock()
            .unwrap()
            .adjusted_heights
            .push((distance, factor, delta));
        Box::pin(async { Ok(()) })
    }

    fn attach(&self, requests: Vec<AttachmentRequest>, _: CancellationToken) -> ActionFuture<'_> {
        let calls = requests
            .iter()
            .map(|request| AttachmentCall {
                item_id: request.item_id(),
                point: request.attachment_point(),
                replace: request.replace_existing_attachments(),
            })
            .collect();
        self.0.lock().unwrap().attachments.push(calls);
        Box::pin(async { Ok(()) })
    }

    fn detach(&self, item_ids: Vec<Guid>, _: CancellationToken) -> ActionFuture<'_> {
        self.0.lock().unwrap().detachments.push(item_ids);
        Box::pin(async { Ok(()) })
    }

    fn rem_outfit(&self, item_ids: Vec<Guid>, _: CancellationToken) -> ActionFuture<'_> {
        self.0.lock().unwrap().removed_outfits.push(item_ids);
        Box::pin(async { Ok(()) })
    }

    fn send_instant_message(
        &self,
        target: Guid,
        message: String,
        _: CancellationToken,
    ) -> ActionFuture<'_> {
        self.0
            .lock()
            .unwrap()
            .instant_messages
            .push((target, message));
        Box::pin(async { Ok(()) })
    }

    fn send_reply(&self, channel: i32, message: String, _: CancellationToken) -> ActionFuture<'_> {
        self.0.lock().unwrap().replies.push((channel, message));
        Box::pin(async { Ok(()) })
    }

    fn set_cam_fov(&self, value: f32, _: CancellationToken) -> ActionFuture<'_> {
        self.0.lock().unwrap().camera_fovs.push(value);
        Box::pin(async { Ok(()) })
    }

    fn set_debug(&self, name: String, value: String, _: CancellationToken) -> ActionFuture<'_> {
        self.0.lock().unwrap().debug_settings.push((name, value));
        Box::pin(async { Ok(()) })
    }

    fn set_env(&self, name: String, value: String, _: CancellationToken) -> ActionFuture<'_> {
        self.0
            .lock()
            .unwrap()
            .environment_settings
            .push((name, value));
        Box::pin(async { Ok(()) })
    }

    fn set_group_with_guid_string_cancellation_token(
        &self,
        id: Guid,
        role: Option<String>,
        _: CancellationToken,
    ) -> ActionFuture<'_> {
        self.0.lock().unwrap().group_ids.push((id, role));
        Box::pin(async { Ok(()) })
    }

    fn set_group_with_string_string_cancellation_token(
        &self,
        name: String,
        role: Option<String>,
        _: CancellationToken,
    ) -> ActionFuture<'_> {
        self.0.lock().unwrap().group_names.push((name, role));
        Box::pin(async { Ok(()) })
    }

    fn set_rot(&self, angle: f32, _: CancellationToken) -> ActionFuture<'_> {
        self.0.lock().unwrap().rotations.push(angle);
        Box::pin(async { Ok(()) })
    }

    fn sit(&self, target: Guid, _: CancellationToken) -> ActionFuture<'_> {
        self.0.lock().unwrap().sits.push(target);
        Box::pin(async { Ok(()) })
    }

    fn sit_ground(&self, _: CancellationToken) -> ActionFuture<'_> {
        self.0.lock().unwrap().sit_ground_count += 1;
        Box::pin(async { Ok(()) })
    }

    fn tp_to(
        &self,
        x: f32,
        y: f32,
        z: f32,
        region: Option<String>,
        look_at: Option<Option<f32>>,
        _: CancellationToken,
    ) -> ActionFuture<'_> {
        self.0.lock().unwrap().teleports.push(TeleportCall {
            x,
            y,
            z,
            region,
            look_at,
        });
        Box::pin(async { Ok(()) })
    }

    fn unsit(&self, _: CancellationToken) -> ActionFuture<'_> {
        self.0.lock().unwrap().unsit_count += 1;
        Box::pin(async { Ok(()) })
    }
}

#[derive(Default)]
pub struct QueryState {
    pub is_sitting: bool,
    pub object_exists: HashMap<Guid, bool>,
    pub active_group: Option<String>,
    pub camera_settings: Option<[f32; 6]>,
    pub debug_settings: BTreeMap<String, String>,
    pub environment_settings: BTreeMap<String, String>,
    pub inventory_map: Option<InventoryMap>,
    pub sit_id: Option<Guid>,
}

#[derive(Clone, Default)]
pub struct RecordingQueries(pub Arc<Mutex<QueryState>>);

type QueryFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, Error>> + Send + 'a>>;

impl IRlvQueryCallbacks for RecordingQueries {
    fn is_sitting(&self, _: CancellationToken) -> QueryFuture<'_, bool> {
        let value = self.0.lock().unwrap().is_sitting;
        Box::pin(async move { Ok(value) })
    }

    fn object_exists(&self, id: Guid, _: CancellationToken) -> QueryFuture<'_, bool> {
        let value = self
            .0
            .lock()
            .unwrap()
            .object_exists
            .get(&id)
            .copied()
            .unwrap_or(false);
        Box::pin(async move { Ok(value) })
    }

    fn try_get_active_group_name(&self, _: CancellationToken) -> QueryFuture<'_, (bool, String)> {
        let value = self.0.lock().unwrap().active_group.clone();
        Box::pin(async move { Ok(value.map_or((false, String::new()), |name| (true, name))) })
    }

    fn try_get_camera_settings(
        &self,
        _: CancellationToken,
    ) -> QueryFuture<'_, (bool, Option<CameraSettings>)> {
        let values = self.0.lock().unwrap().camera_settings;
        Box::pin(async move {
            let Some([av_min, av_max, fov_min, fov_max, zoom_min, current_fov]) = values else {
                return Ok((false, None));
            };
            Ok((
                true,
                Some(CameraSettings::new(
                    av_min,
                    av_max,
                    fov_min,
                    fov_max,
                    zoom_min,
                    current_fov,
                )?),
            ))
        })
    }

    fn try_get_debug_setting_value(
        &self,
        name: String,
        _: CancellationToken,
    ) -> QueryFuture<'_, (bool, String)> {
        let value = self.0.lock().unwrap().debug_settings.get(&name).cloned();
        Box::pin(async move { Ok(value.map_or((false, String::new()), |value| (true, value))) })
    }

    fn try_get_environment_setting_value(
        &self,
        name: String,
        _: CancellationToken,
    ) -> QueryFuture<'_, (bool, String)> {
        let value = self
            .0
            .lock()
            .unwrap()
            .environment_settings
            .get(&name)
            .cloned();
        Box::pin(async move { Ok(value.map_or((false, String::new()), |value| (true, value))) })
    }

    fn try_get_inventory_map(
        &self,
        _: CancellationToken,
    ) -> QueryFuture<'_, (bool, Option<InventoryMap>)> {
        let value = self.0.lock().unwrap().inventory_map.take();
        Box::pin(async move { Ok((value.is_some(), value)) })
    }

    fn try_get_sit_id(&self, _: CancellationToken) -> QueryFuture<'_, (bool, Guid)> {
        let value = self.0.lock().unwrap().sit_id;
        Box::pin(async move { Ok(value.map_or((false, Guid([0; 16])), |id| (true, id))) })
    }
}

pub struct RlvHarness {
    pub service: RlvService,
    pub actions: Arc<Mutex<ActionLog>>,
    pub queries: Arc<Mutex<QueryState>>,
}

impl RlvHarness {
    /// Builds the same enabled service and two narrow callback fakes as the C# base fixture.
    ///
    /// # Errors
    ///
    /// Returns the standardized constructor error while the public RLV service is a shim.
    pub fn new() -> Result<Self, Error> {
        let action_callbacks = RecordingActions::default();
        let query_callbacks = RecordingQueries::default();
        let actions = Arc::clone(&action_callbacks.0);
        let queries = Arc::clone(&query_callbacks.0);
        let service = RlvService::new(Box::new(query_callbacks), Box::new(action_callbacks), true)?;
        Ok(Self {
            service,
            actions,
            queries,
        })
    }

    /// Processes a command using the base fixture sender identity.
    ///
    /// # Errors
    ///
    /// Returns the public RLV service error for the command.
    pub async fn process(&self, command: &str) -> Result<bool, Error> {
        self.service
            .process_message(command.into(), guid(SENDER_ID), SENDER_NAME.into(), None)
            .await
    }

    /// Processes a command using an explicit sender identity.
    ///
    /// # Errors
    ///
    /// Returns the public RLV service error for the command.
    pub async fn process_from(
        &self,
        command: &str,
        sender_id: &str,
        sender_name: &str,
    ) -> Result<bool, Error> {
        self.service
            .process_message(command.into(), guid(sender_id), sender_name.into(), None)
            .await
    }
}

pub struct SampleInventoryTree {
    pub root: RlvSharedFolder,
    pub clothing: RlvSharedFolder,
    pub hats: RlvSharedFolder,
    pub sub_hats: RlvSharedFolder,
    pub accessories: RlvSharedFolder,
    pub fancy_hat: RlvInventoryItem,
    pub party_hat: RlvInventoryItem,
    pub business_pants: RlvInventoryItem,
    pub retro_pants: RlvInventoryItem,
    pub happy_shirt: RlvInventoryItem,
    pub glasses: RlvInventoryItem,
    pub watch: RlvInventoryItem,
}

impl SampleInventoryTree {
    /// Builds the exact `#RLV` hierarchy shared by the upstream fixtures.
    ///
    /// # Errors
    ///
    /// Returns a mapped constructor or collection error.
    pub fn build() -> Result<Self, Error> {
        let root =
            RlvSharedFolder::new(guid("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"), "#RLV".into())?;
        let clothing = root.add_child(
            guid("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
            "Clothing".into(),
        )?;
        let hats =
            clothing.add_child(guid("dddddddd-dddd-4ddd-8ddd-dddddddddddd"), "Hats".into())?;
        let sub_hats = hats.add_child(
            guid("ffffffff-0000-4000-8000-000000000000"),
            "Sub Hats".into(),
        )?;
        root.add_child(
            guid("eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee"),
            ".private".into(),
        )?;
        let accessories = root.add_child(
            guid("cccccccc-cccc-4ccc-8ccc-cccccccccccc"),
            "Accessories".into(),
        )?;
        let watch = accessories.add_item(
            guid("c0000000-cccc-4ccc-8ccc-cccccccccccc"),
            "Watch".into(),
            false,
            None,
            None,
            None,
            None,
        )?;
        let glasses = accessories.add_item(
            guid("c1111111-cccc-4ccc-8ccc-cccccccccccc"),
            "Glasses".into(),
            false,
            None,
            None,
            None,
            None,
        )?;
        let business_pants = clothing.add_item(
            guid("b0000000-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
            "Business Pants (Pelvis)".into(),
            false,
            None,
            None,
            None,
            None,
        )?;
        let happy_shirt = clothing.add_item(
            guid("b1111111-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
            "Happy Shirt".into(),
            false,
            None,
            None,
            None,
            None,
        )?;
        let retro_pants = clothing.add_item(
            guid("b2222222-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
            "Retro Pants".into(),
            false,
            None,
            None,
            None,
            None,
        )?;
        let party_hat = hats.add_item(
            guid("d0000000-dddd-4ddd-8ddd-dddddddddddd"),
            "Party Hat (Spine)".into(),
            false,
            None,
            None,
            None,
            None,
        )?;
        let fancy_hat = hats.add_item(
            guid("d1111111-dddd-4ddd-8ddd-dddddddddddd"),
            "Fancy Hat (chin)".into(),
            false,
            None,
            None,
            None,
            None,
        )?;
        Ok(Self {
            root,
            clothing,
            hats,
            sub_hats,
            accessories,
            fancy_hat,
            party_hat,
            business_pants,
            retro_pants,
            happy_shirt,
            glasses,
            watch,
        })
    }
}
