//! Native implementations of the upstream connection and realism helpers.

#![allow(clippy::missing_errors_doc)] // Signatures and error semantics mirror the mapped C# API.

use libremetaverse::{ChatType, GridClient};
use libremetaverse_types::compat::{CancellationToken, CancellationTokenSource};
use libremetaverse_types::{Error, UUID, Vector3};
use std::sync::{Arc, Mutex, MutexGuard, mpsc};
use std::thread::JoinHandle;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

const TYPE_ANIMATION: &str = "c541c47f-e0c0-058b-ad1a-d6ae3a4584d9";

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

struct CheckLoop {
    stop: mpsc::Sender<()>,
    cancellation: Arc<CancellationTokenSource>,
    worker: JoinHandle<()>,
}

/// Keeps a client in one simulator and provides the upstream persistent-login policy.
pub struct ConnectionManager {
    client: GridClient,
    timer_frequency: Duration,
    destination: Arc<Mutex<(u64, Vector3)>>,
    check_loop: Mutex<Option<CheckLoop>>,
}

impl ConnectionManager {
    /// Creates an idle connection manager. Monitoring starts with [`Self::stay_in_sim`].
    pub fn new(client: GridClient, timer_frequency: i32) -> Result<Self, Error> {
        let milliseconds = u64::try_from(timer_frequency).map_err(|_| Error::Argument)?;
        if milliseconds == 0 {
            return Err(Error::Argument);
        }
        Ok(Self {
            client,
            timer_frequency: Duration::from_millis(milliseconds),
            destination: Arc::new(Mutex::new((0, Vector3::zero()))),
            check_loop: Mutex::new(None),
        })
    }

    /// Retries transient login errors with the delays defined by `LibreMetaverse`.
    pub async fn persistent_login(
        client: GridClient,
        first_name: String,
        last_name: String,
        password: String,
        user_agent: String,
        start: String,
        author: String,
    ) -> Result<bool, Error> {
        let mut unknown_logins = 0_u8;
        loop {
            if client
                .network()
                .login_with_string_string_string_string_string_string_cancellation_token(
                    first_name.clone(),
                    last_name.clone(),
                    password.clone(),
                    user_agent.clone(),
                    start.clone(),
                    author.clone(),
                    None,
                )
                .await?
            {
                return Ok(true);
            }

            let wait = match client.network().login_error_key().as_str() {
                "god" => Duration::from_mins(10),
                "key" | "disabled" => return Ok(false),
                "presence" | "timed out" | "no connection" | "bad response" => {
                    Duration::from_mins(1)
                }
                _ => {
                    unknown_logins += 1;
                    if unknown_logins >= 5 {
                        return Ok(false);
                    }
                    Duration::from_mins(2)
                }
            };
            tokio::time::sleep(wait).await;
        }
    }

    /// Starts (or retargets) the background simulator-presence check.
    pub fn stay_in_sim(&self, handle: u64, desired_position: Vector3) -> Result<(), Error> {
        *lock(&self.destination) = (handle, desired_position);
        let mut active = lock(&self.check_loop);
        if active.is_some() {
            return Ok(());
        }

        let mut client = self.client.clone();
        let destination = Arc::clone(&self.destination);
        let frequency = self.timer_frequency;
        let cancellation = Arc::new(CancellationTokenSource::new());
        let token = cancellation.token();
        let (stop, stopped) = mpsc::channel();
        let worker = std::thread::Builder::new()
            .name("libremetaverse-stay-in-sim".into())
            .spawn(move || {
                let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
                    .enable_time()
                    .build()
                else {
                    return;
                };
                loop {
                    if token.is_cancellation_requested() {
                        break;
                    }
                    let (target_handle, target_position) = *lock(&destination);
                    if target_handle != 0
                        && let Some(current) = client.network().current_sim()
                        && current.handle != 0
                        && current.handle != target_handle
                    {
                        let _ = runtime.block_on(
                            client
                                .self_()
                                .teleport_with_u_int64_vector3_cancellation_token(
                                    target_handle,
                                    target_position,
                                    Some(token.clone()),
                                ),
                        );
                    }
                    match stopped.recv_timeout(frequency) {
                        Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => break,
                        Err(mpsc::RecvTimeoutError::Timeout) => {}
                    }
                }
            })
            .map_err(|_| Error::InvalidOperation)?;
        *active = Some(CheckLoop {
            stop,
            cancellation,
            worker,
        });
        Ok(())
    }

    /// Cancels and joins the active presence check. Calling it repeatedly is safe.
    pub fn stop(&self) -> Result<(), Error> {
        let Some(active) = lock(&self.check_loop).take() else {
            return Ok(());
        };
        active.cancellation.cancel();
        let _ = active.stop.send(());
        active.worker.join().map_err(|_| Error::InvalidOperation)
    }
}

impl Drop for ConnectionManager {
    fn drop(&mut self) {
        let _ = self.stop();
    }
}

/// Human-like chat and mouselook helpers.
pub struct Realism;

impl Realism {
    /// Sends normal chat at the upstream default of three characters per second.
    pub fn chat_with_grid_client_string(client: GridClient, message: String) -> Result<(), Error> {
        Self::chat_with_grid_client_string_chat_type_int32(client, message, ChatType::Normal, 3)
    }

    /// Blocking compatibility wrapper around the asynchronous typing sequence.
    pub fn chat_with_grid_client_string_chat_type_int32(
        client: GridClient,
        message: String,
        type_: ChatType,
        cps: i32,
    ) -> Result<(), Error> {
        std::thread::Builder::new()
            .name("libremetaverse-realism-chat".into())
            .spawn(move || {
                let runtime = tokio::runtime::Builder::new_current_thread()
                    .enable_time()
                    .build()
                    .map_err(|_| Error::InvalidOperation)?;
                runtime.block_on(
                    Self::chat_with_grid_client_string_chat_type_int32_cancellation_token(
                        client, message, type_, cps, None,
                    ),
                )
            })
            .map_err(|_| Error::InvalidOperation)?
            .join()
            .map_err(|_| Error::InvalidOperation)?
    }

    /// Emits start/stop typing chat and animation traffic while delaying by `cps`.
    pub async fn chat_with_grid_client_string_chat_type_int32_cancellation_token(
        mut client: GridClient,
        message: String,
        type_: ChatType,
        cps: i32,
        cancellation_token: Option<CancellationToken>,
    ) -> Result<(), Error> {
        let cps = usize::try_from(cps)
            .ok()
            .filter(|value| *value > 0)
            .ok_or(Error::Argument)?;
        let cancellation = cancellation_token.unwrap_or_default();
        cancellation.throw_if_cancellation_requested()?;
        let animation = UUID::new_with_string(TYPE_ANIMATION.into())?;
        let agent = client.self_();
        agent.chat(String::new(), 0, ChatType::StartTyping, None)?;
        if let Err(error) = agent.animation_start(animation, false) {
            let _ = agent.chat(String::new(), 0, ChatType::StopTyping, None);
            return Err(error);
        }

        let mut characters = 0_usize;
        let mut typing = true;
        let outcome = async {
            while characters < message.chars().count() {
                cancellation.throw_if_cancellation_requested()?;
                if !typing {
                    agent.chat(String::new(), 0, ChatType::StartTyping, None)?;
                    agent.animation_start(animation, false)?;
                    typing = true;
                } else if pseudo_random_tenth() {
                    agent.chat(String::new(), 0, ChatType::StopTyping, None)?;
                    agent.animation_stop(animation, false)?;
                    typing = false;
                }
                tokio::select! {
                    () = tokio::time::sleep(Duration::from_secs(1)) => {}
                    () = cancellation.cancelled() => return Err(Error::Cancelled),
                }
                characters = characters.saturating_add(cps);
            }
            agent.chat(message, 0, type_, None)
        }
        .await;

        let stop_chat = agent.chat(String::new(), 0, ChatType::StopTyping, None);
        let stop_animation = agent.animation_stop(animation, false);
        outcome.and(stop_chat).and(stop_animation)
    }

    /// Performs the mouselook press/release sequence when agent updates are enabled.
    pub fn shoot_with_grid_client(mut client: GridClient) -> Result<bool, Error> {
        if !client.settings().agent().send_updates {
            return Ok(false);
        }
        let movement = &mut client.self_().movement;
        movement.set_mouselook(true);
        movement.set_ml_button_down(true);
        let outcome = (|| {
            movement.send_update_with_boolean(None)?;
            movement.set_ml_button_up(true);
            movement.set_ml_button_down(false);
            movement.set_finish_anim(true);
            movement.send_update_with_boolean(None)?;
            movement.set_mouselook(false);
            movement.set_ml_button_up(false);
            movement.set_finish_anim(false);
            movement.send_update_with_boolean(None)
        })();
        movement.set_mouselook(false);
        movement.set_ml_button_down(false);
        movement.set_ml_button_up(false);
        movement.set_finish_anim(false);
        outcome.map(|()| true)
    }

    /// Turns toward `target` before performing the mouselook press/release sequence.
    pub fn shoot_with_grid_client_vector3(
        mut client: GridClient,
        target: Vector3,
    ) -> Result<bool, Error> {
        if !client.self_().movement.turn_toward(target, None)? {
            return Ok(false);
        }
        Self::shoot_with_grid_client(client)
    }
}

fn pseudo_random_tenth() -> bool {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_or(0, |duration| duration.subsec_nanos())
        % 10
        == 9
}

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

    #[test]
    fn connection_manager_rejects_non_positive_frequency() {
        assert!(matches!(
            ConnectionManager::new(GridClient::new().unwrap(), 0),
            Err(Error::Argument)
        ));
        assert!(matches!(
            ConnectionManager::new(GridClient::new().unwrap(), -1),
            Err(Error::Argument)
        ));
    }

    #[test]
    fn connection_monitor_can_retarget_and_stop_without_a_login() {
        let manager = ConnectionManager::new(GridClient::new().unwrap(), 10).unwrap();
        manager.stay_in_sim(1, Vector3::unit_x()).unwrap();
        manager.stay_in_sim(2, Vector3::unit_y()).unwrap();
        manager.stop().unwrap();
        manager.stop().unwrap();
    }

    #[test]
    fn shoot_reports_disabled_updates_without_network_traffic() {
        let mut client = GridClient::new().unwrap();
        client.settings().agent_mut().send_updates = false;
        assert!(!Realism::shoot_with_grid_client(client).unwrap());
    }

    #[tokio::test(flavor = "current_thread")]
    async fn chat_rejects_zero_rate_before_network_traffic() {
        let error = Realism::chat_with_grid_client_string_chat_type_int32_cancellation_token(
            GridClient::new().unwrap(),
            "hello".into(),
            ChatType::Normal,
            0,
            None,
        )
        .await
        .unwrap_err();
        assert_eq!(error, Error::Argument);
    }

    #[tokio::test(flavor = "current_thread")]
    async fn chat_honors_pre_cancelled_token_before_network_traffic() {
        let source = CancellationTokenSource::new();
        source.cancel();
        let error = Realism::chat_with_grid_client_string_chat_type_int32_cancellation_token(
            GridClient::new().unwrap(),
            "hello".into(),
            ChatType::Normal,
            3,
            Some(source.token()),
        )
        .await
        .unwrap_err();
        assert_eq!(error, Error::Cancelled);
    }
}
