//! Credential-safe native `TestClient` voice capability commands.

use super::{FakeBackend, InventoryFuture as BackendFuture, LiveBackend, lock};
use libremetaverse::types::compat::{CancellationToken, Uri};
use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser};
use std::collections::HashMap;
use std::time::Duration;

const MAX_RESPONSE_BYTES: usize = 1024 * 1024;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);

#[derive(Clone, Copy)]
pub(super) enum Command {
    Parcel,
    Account,
}

pub(super) fn commands() -> [super::CommandEntry; 2] {
    use super::{CommandCategory as Cat, CommandHandler};
    [
        (
            "voiceparcel",
            "Obtain redacted parcel voice information. Usage: voiceparcel",
            Cat::Other,
            CommandHandler::Voice(Command::Parcel),
        ),
        (
            "voiceaccount",
            "Provision voice account information with credentials redacted. Usage: voiceaccount",
            Cat::Other,
            CommandHandler::Voice(Command::Account),
        ),
    ]
}

pub(super) trait Backend {
    fn voice_query(
        &self,
        command: Command,
        cancellation: CancellationToken,
    ) -> BackendFuture<'_, Result<VoiceResult, String>>;
}

pub(super) enum VoiceResult {
    Parcel {
        region_name_present: bool,
        parcel_local_id: i32,
        channel_present: bool,
    },
    Account {
        username_present: bool,
        password_present: bool,
    },
}

pub(super) async fn execute<B: Backend + ?Sized>(
    backend: &B,
    command: Command,
    args: &[String],
    cancellation: CancellationToken,
) -> String {
    if !args.is_empty() {
        return match command {
            Command::Parcel => "Usage: voiceparcel",
            Command::Account => "Usage: voiceaccount",
        }
        .into();
    }
    match backend.voice_query(command, cancellation).await {
        Ok(VoiceResult::Parcel {
            region_name_present,
            parcel_local_id,
            channel_present,
        }) => format!(
            "Parcel voice info: region-name-present={region_name_present}, parcel-local-id={parcel_local_id}, channel=<{}>",
            if channel_present {
                "redacted"
            } else {
                "missing"
            }
        ),
        Ok(VoiceResult::Account {
            username_present,
            password_present,
        }) => format!(
            "Voice account provisioned: credentials=<{}>",
            if username_present && password_present {
                "redacted"
            } else {
                "incomplete"
            }
        ),
        Err(error) => error,
    }
}

impl Backend for FakeBackend {
    fn voice_query(
        &self,
        command: Command,
        cancellation: CancellationToken,
    ) -> BackendFuture<'_, Result<VoiceResult, String>> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err("Voice capability request cancelled".into());
            }
            Ok(match command {
                Command::Parcel => {
                    self.record("CALL voice-parcel-info".into());
                    VoiceResult::Parcel {
                        region_name_present: true,
                        parcel_local_id: 42,
                        channel_present: true,
                    }
                }
                Command::Account => {
                    self.record("CALL voice-provision-account credentials=<redacted>".into());
                    VoiceResult::Account {
                        username_present: true,
                        password_present: true,
                    }
                }
            })
        })
    }
}

impl Backend for LiveBackend {
    fn voice_query(
        &self,
        command: Command,
        cancellation: CancellationToken,
    ) -> BackendFuture<'_, Result<VoiceResult, String>> {
        Box::pin(async move {
            let capability = match command {
                Command::Parcel => "ParcelVoiceInfoRequest",
                Command::Account => "ProvisionVoiceAccountRequest",
            };
            let simulator = self
                .network
                .current_sim()
                .ok_or_else(|| "No current simulator is available".to_owned())?;
            let uri = simulator
                .native_capability_uri(capability)
                .map_err(|_| "Voice capability lookup failed".to_owned())?
                .ok_or_else(|| "Required voice capability is unavailable".to_owned())?;
            let http = lock(&self.client).http_caps_client();
            let request = http.post_with_uri_osd_format_osd_cancellation_token_i_progress(
                Uri(uri.0),
                OSDFormat::Xml,
                OSD::Map(HashMap::new()),
                cancellation.clone(),
                None,
            );
            let result = tokio::select! {
                () = cancellation.cancelled() => return Err("Voice capability request cancelled".into()),
                () = tokio::time::sleep(REQUEST_TIMEOUT) => return Err("Voice capability request timed out".into()),
                result = request => result,
            }
            .map_err(|_| "Voice capability request failed".to_owned())?;
            let (response, bytes) = result;
            if !response.is_success_status_code() || bytes.len() > MAX_RESPONSE_BYTES {
                return Err("Voice capability returned an error".into());
            }
            let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes)
                .map_err(|_| "Voice capability returned malformed LLSD".to_owned())?
            else {
                return Err("Voice capability response was not an LLSD map".into());
            };
            Ok(match command {
                Command::Parcel => {
                    let region_name_present = present_string(&map, "region_name");
                    let parcel_local_id = map
                        .get("parcel_local_id")
                        .and_then(|value| value.as_integer().ok())
                        .ok_or_else(|| "Parcel voice response omitted local ID".to_owned())?;
                    let channel_present = match map.get("voice_credentials") {
                        Some(OSD::Map(credentials)) => present_string(credentials, "channel_uri"),
                        _ => false,
                    };
                    if !region_name_present || !channel_present {
                        return Err("Parcel voice response omitted required fields".into());
                    }
                    VoiceResult::Parcel {
                        region_name_present,
                        parcel_local_id,
                        channel_present,
                    }
                }
                Command::Account => {
                    let username_present = present_string(&map, "username");
                    let password_present = present_string(&map, "password");
                    if !username_present || !password_present {
                        return Err("Voice account response omitted credentials".into());
                    }
                    VoiceResult::Account {
                        username_present,
                        password_present,
                    }
                }
            })
        })
    }
}

fn present_string(map: &HashMap<String, OSD>, key: &str) -> bool {
    map.get(key)
        .and_then(|value| value.as_string().ok())
        .is_some_and(|value| !value.is_empty())
}
