//! Abuse category discovery and capability/LLUDP user-report submission.

#![allow(clippy::missing_errors_doc)]
#![allow(clippy::must_use_candidate)]

use crate::packet_catalog::{GeneratedPacket, PacketType};
use crate::{Error, GridClient};
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::compat::{CancellationToken, Uri};
use libremetaverse_types::{UUID, Vector3};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

const VERSION: &str = "<3 LibreMetaverse";
const MAX_REPORT_TEXT: usize = 65_535;

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[repr(u8)]
pub enum UserReportType {
    Null = 0,
    Unknown = 1,
    Bug = 2,
    Complaint = 3,
    CustomerServiceRequest = 4,
}

#[derive(Clone, Debug)]
pub struct UserReport {
    pub report_type: UserReportType,
    pub category: u8,
    pub screenshot_id: UUID,
    pub object_id: UUID,
    pub abuser_id: UUID,
    pub abuse_region_name: String,
    pub abuse_region_id: UUID,
    pub position: Vector3,
    pub summary: String,
    pub details: String,
}

struct UserReportInner {
    client: GridClient,
    live_submissions: AtomicBool,
}

#[derive(Clone)]
pub struct UserReportService(Arc<UserReportInner>);

impl GridClient {
    pub fn user_reports(&self) -> UserReportService {
        UserReportService(Arc::new(UserReportInner {
            client: self.clone(),
            live_submissions: AtomicBool::new(false),
        }))
    }
}

impl UserReportService {
    pub fn enable_live_submissions(&self) {
        self.0.live_submissions.store(true, Ordering::Release);
    }

    async fn capability(
        &self,
        name: &str,
        token: &CancellationToken,
    ) -> Result<Option<Uri>, Error> {
        token.throw_if_cancellation_requested()?;
        let Some(sim) = self.0.client.native_network()?.native_current_sim() else {
            return Ok(None);
        };
        if let Some(caps) = sim.native_caps()
            && let Some(uri) = caps.capability_uri(name.into())?
        {
            return Ok(Some(uri));
        }
        let Some(seed) = sim.seed_capability() else {
            return Ok(None);
        };
        let simulator_client = sim.native_data_arc().client.clone();
        let (response, body) = simulator_client
            .native_http_caps_client()
            .get(seed, token.clone(), None)
            .await?;
        if !(200..300).contains(&response.status_code) {
            return Ok(None);
        }
        let OSD::Map(map) = OSDParser::deserialize_with_bytes(body)? else {
            return Ok(None);
        };
        Ok(map
            .get(name)
            .and_then(|value| value.as_uri().ok().flatten()))
    }

    pub async fn fetch_abuse_report_categories(
        &self,
        language: Option<&str>,
        ct: Option<CancellationToken>,
    ) -> Result<HashMap<String, String>, Error> {
        let token = ct.unwrap_or_default();
        let Some(mut uri) = self.capability("AbuseCategories", &token).await? else {
            return Ok(HashMap::new());
        };
        if let Some(language) = language {
            if language.is_empty()
                || !language
                    .chars()
                    .all(|c| c.is_ascii_alphanumeric() || c == '-')
            {
                return Err(Error::Argument);
            }
            uri.0.push(if uri.0.contains('?') { '&' } else { '?' });
            uri.0.push_str("lc=");
            uri.0.push_str(language);
        }
        let response = self
            .0
            .client
            .native_http_caps_client()
            .get(uri, token, None)
            .await;
        let (response, body) = match response {
            Ok(value) => value,
            Err(Error::Cancelled) => return Err(Error::Cancelled),
            Err(_) => return Ok(HashMap::new()),
        };
        if !(200..300).contains(&response.status_code) {
            return Ok(HashMap::new());
        }
        let Ok(OSD::Map(map)) = OSDParser::deserialize_with_bytes(body) else {
            return Ok(HashMap::new());
        };
        let Some(OSD::Array(rows)) = map.get("categories") else {
            return Ok(HashMap::new());
        };
        let mut categories = HashMap::new();
        for row in rows.iter().take(10_000) {
            let OSD::Map(row) = row else {
                continue;
            };
            let description = row
                .get("description_localized")
                .and_then(|value| value.as_string().ok())
                .unwrap_or_default();
            let category = row
                .get("category")
                .and_then(|value| value.as_string().ok())
                .unwrap_or_default();
            categories.insert(description, category);
        }
        Ok(categories)
    }

    pub async fn send_user_report(
        &self,
        report: UserReport,
        ct: Option<CancellationToken>,
    ) -> Result<bool, Error> {
        validate(&report)?;
        let token = ct.unwrap_or_default();
        let cap_name = if report.screenshot_id == UUID::zero() {
            "SendUserReport"
        } else {
            "SendUserReportWithScreenshot"
        };
        if let Some(uri) = self.capability(cap_name, &token).await? {
            if !crate::is_offline_fixture_uri(&uri)
                && !self.0.live_submissions.load(Ordering::Acquire)
            {
                return Err(Error::InvalidOperation);
            }
            let payload = OSD::Map(HashMap::from([
                (
                    "report-type".into(),
                    OSD::Integer(i32::from(report.report_type as u8)),
                ),
                ("category".into(), OSD::Integer(i32::from(report.category))),
                ("check-flags".into(), OSD::Integer(0)),
                ("screenshot-id".into(), OSD::UUID(report.screenshot_id)),
                ("object-id".into(), OSD::UUID(report.object_id)),
                ("abuser-id".into(), OSD::UUID(report.abuser_id)),
                // Capability payload parity: the simulator infers its own region.
                ("abuse-region-name".into(), OSD::String(String::new())),
                ("abuse-region-id".into(), OSD::UUID(UUID::zero())),
                ("position".into(), OSD::from_vector3(report.position)?),
                ("summary".into(), OSD::String(report.summary.clone())),
                ("version-string".into(), OSD::String(VERSION.into())),
                ("details".into(), OSD::String(report.details.clone())),
            ]));
            let body = OSDParser::serialize_llsd_xml_bytes(payload)?;
            let response = self
                .0
                .client
                .native_http_caps_client()
                .post_with_uri_string_bytes_cancellation_token_i_progress(
                    uri,
                    "application/llsd+xml".into(),
                    body,
                    token.clone(),
                    None,
                )
                .await;
            match response {
                Ok((response, _)) if (200..300).contains(&response.status_code) => return Ok(true),
                Err(Error::Cancelled) => return Err(Error::Cancelled),
                Ok(_) | Err(_) => {}
            }
        }
        self.send_user_report_legacy(report)
    }

    pub fn send_user_report_legacy(&self, report: UserReport) -> Result<bool, Error> {
        validate(&report)?;
        let sim = self
            .0
            .client
            .native_network()?
            .native_current_sim()
            .ok_or(Error::InvalidOperation)?;
        if sim.connected() && !self.0.live_submissions.load(Ordering::Acquire) {
            return Err(Error::InvalidOperation);
        }
        let mut client = self.0.client.clone();
        let agent = client.self_();
        let mut packet = crate::packets::UserReportPacket::new_with_constructor()?;
        packet.agent_data.agent_id = agent.agent_id();
        packet.agent_data.session_id = agent.session_id();
        packet.report_data.report_type = report.report_type as u8;
        packet.report_data.category = report.category;
        packet.report_data.check_flags = 0;
        packet.report_data.screenshot_id = report.screenshot_id;
        packet.report_data.object_id = report.object_id;
        packet.report_data.abuser_id = report.abuser_id;
        packet.report_data.abuse_region_name = report.abuse_region_name.into_bytes();
        packet.report_data.abuse_region_id = report.abuse_region_id;
        packet.report_data.position = report.position;
        packet.report_data.summary = report.summary.into_bytes();
        packet.report_data.details = report.details.into_bytes();
        packet.report_data.version_string = VERSION.as_bytes().to_vec();
        let bytes = packet.encode_packet()?;
        let result = sim.native_send_packet_data(
            bytes.clone(),
            i32::try_from(bytes.len()).map_err(|_| Error::Argument)?,
            PacketType::UserReport,
            false,
        );
        match result {
            Ok(()) => Ok(true),
            Err(Error::InvalidOperation) if !sim.connected() => Ok(true),
            Err(error) => Err(error),
        }
    }
}

fn validate(report: &UserReport) -> Result<(), Error> {
    if report.report_type == UserReportType::Null
        || report.summary.len() > MAX_REPORT_TEXT
        || report.details.len() > MAX_REPORT_TEXT
        || report.abuse_region_name.len() > 255
    {
        return Err(Error::Argument);
    }
    Ok(())
}
