//! Runtime LSL keyword catalog with OpenSim capability refresh support.

#![allow(clippy::missing_errors_doc)] // Public signatures mirror the compatibility map.

use std::collections::HashMap;
use std::io::Read as _;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock, RwLock};
use std::time::Duration;

use base64::Engine as _;
use flate2::read::GzDecoder;
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::UUID;
use libremetaverse_types::compat::{
    CancellationToken, EventHandler, FrozenDictionary, Subscription, Uri,
};

use crate::{Error, GridClient, LslSyntaxLslCategory, LslSyntaxLslKeyword, Simulator};

const VERSION_KEY: &str = "llsd-lsl-syntax-version";
const SYNTAX_FEATURE: &str = "LSLSyntaxId";
const SYNTAX_CAPABILITY: &str = "LSLSyntax";
const MAX_SYNTAX_BYTES: usize = 8 * 1024 * 1024;
const FETCH_WAIT: Duration = Duration::from_secs(20);
const DEFAULT_SYNTAX_GZIP_BASE64: &str =
    include_str!("../resources/keywords_lsl_default.xml.gz.b64");

static KEYWORDS: OnceLock<RwLock<HashMap<String, LslSyntaxLslKeyword>>> = OnceLock::new();

fn keyword_store() -> &'static RwLock<HashMap<String, LslSyntaxLslKeyword>> {
    KEYWORDS.get_or_init(|| RwLock::new(HashMap::new()))
}

struct SyntaxEvents {
    next_id: AtomicU64,
    handlers: Mutex<Vec<(u64, EventHandler<()>)>>,
}

impl Default for SyntaxEvents {
    fn default() -> Self {
        Self {
            next_id: AtomicU64::new(1),
            handlers: Mutex::new(Vec::new()),
        }
    }
}

struct LslSyntaxInner {
    events: Arc<SyntaxEvents>,
    client: Mutex<Option<GridClient>>,
    syntax_id: Mutex<UUID>,
    subscriptions: Mutex<Vec<Subscription>>,
}

/// Process-wide LSL keyword catalog and per-registration change notification.
#[derive(Clone)]
pub struct LslSyntax(Arc<LslSyntaxInner>);

impl LslSyntax {
    pub fn new_with_constructor() -> Result<Self, Error> {
        let syntax = Self::empty();
        syntax.load_default()?;
        Ok(syntax)
    }

    pub fn new_with_grid_client(client: GridClient) -> Result<Self, Error> {
        let syntax = Self::empty();
        syntax.register(client)?;
        Ok(syntax)
    }

    fn empty() -> Self {
        Self(Arc::new(LslSyntaxInner {
            events: Arc::new(SyntaxEvents::default()),
            client: Mutex::new(None),
            syntax_id: Mutex::new(UUID::zero()),
            subscriptions: Mutex::new(Vec::new()),
        }))
    }

    pub fn subscribe_syntax_changed(&self, handler: EventHandler<()>) -> Subscription {
        let id = self.0.events.next_id.fetch_add(1, Ordering::Relaxed);
        self.0
            .events
            .handlers
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .push((id, handler));
        let events = Arc::downgrade(&self.0.events);
        Subscription::new(move || {
            if let Some(events) = events.upgrade() {
                events
                    .handlers
                    .lock()
                    .unwrap_or_else(std::sync::PoisonError::into_inner)
                    .retain(|(candidate, _)| *candidate != id);
            }
        })
    }

    pub fn keywords() -> FrozenDictionary<String, LslSyntaxLslKeyword> {
        FrozenDictionary(
            keyword_store()
                .read()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .clone(),
        )
    }

    pub fn register(&self, client: GridClient) -> Result<(), Error> {
        *self
            .0
            .client
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(client.clone());

        // Always establish a useful catalog, including on OpenSim grids that
        // do not advertise the optional LSLSyntax capability.
        self.load_default()?;

        let inner = Arc::clone(&self.0);
        let network = client.native_network()?;
        let client_for_change = client.clone();
        let changed = network.native_subscribe_sim_changed(Arc::new(move |_| {
            if let Ok(network) = client_for_change.native_network()
                && let Some(simulator) = network.native_current_sim()
            {
                LslSyntaxInner::observe_simulator(&inner, simulator, false);
            }
        }));
        self.retain_subscription(changed);

        if let Some(simulator) = network.native_current_sim() {
            LslSyntaxInner::observe_simulator(&self.0, simulator, true);
        }
        Ok(())
    }

    fn retain_subscription(&self, subscription: Subscription) {
        self.0
            .subscriptions
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .push(subscription);
    }

    fn load_default(&self) -> Result<(), Error> {
        let compact = DEFAULT_SYNTAX_GZIP_BASE64
            .split_ascii_whitespace()
            .collect::<String>();
        let compressed = base64::engine::general_purpose::STANDARD
            .decode(compact)
            .map_err(|_| Error::InvalidOperation)?;
        let mut decoder = GzDecoder::new(compressed.as_slice());
        let mut bytes = Vec::new();
        decoder
            .by_ref()
            .take((MAX_SYNTAX_BYTES + 1) as u64)
            .read_to_end(&mut bytes)
            .map_err(|_| Error::InvalidOperation)?;
        if bytes.len() > MAX_SYNTAX_BYTES {
            return Err(Error::Argument);
        }
        self.0.parse_and_publish(&bytes)
    }
}

impl LslSyntaxInner {
    fn observe_simulator(this: &Arc<Self>, simulator: Simulator, wait: bool) {
        if let Some(caps) = simulator.native_caps() {
            let weak = Arc::downgrade(this);
            let subscription = caps.subscribe_capabilities_received(Some(Arc::new(move |args| {
                if let Some(inner) = weak.upgrade() {
                    inner.refresh(args.simulator(), false);
                }
            })));
            this.subscriptions
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .push(subscription);
        }
        this.refresh(simulator, wait);
    }

    fn refresh(self: &Arc<Self>, simulator: Simulator, wait: bool) {
        let syntax_id = match simulator.features.get(SYNTAX_FEATURE.to_owned()) {
            Ok(Some(value)) => match value.as_uuid() {
                Ok(value) => value,
                Err(_) => return,
            },
            _ => return,
        };
        if *self
            .syntax_id
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            == syntax_id
        {
            return;
        }

        let Some(client) = self
            .client
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
        else {
            return;
        };
        let cache = cache_path(&client, syntax_id);
        if let Ok(bytes) = std::fs::read(&cache)
            && self.parse_and_publish(&bytes).is_ok()
        {
            *self
                .syntax_id
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner) = syntax_id;
            return;
        }
        let Ok(Some(uri)) = simulator.native_capability_uri(SYNTAX_CAPABILITY) else {
            return;
        };
        self.fetch(client, uri, syntax_id, cache, wait);
    }

    fn fetch(
        self: &Arc<Self>,
        client: GridClient,
        uri: Uri,
        syntax_id: UUID,
        cache: PathBuf,
        wait: bool,
    ) {
        let inner = Arc::clone(self);
        let (completed, receiver) = std::sync::mpsc::sync_channel(1);
        let spawn = std::thread::Builder::new()
            .name("lsl-syntax-fetch".to_owned())
            .spawn(move || {
                let outcome = fetch_bytes(&client, uri).and_then(|bytes| {
                    inner.parse_and_publish(&bytes)?;
                    persist_cache(&cache, &bytes)?;
                    *inner
                        .syntax_id
                        .lock()
                        .unwrap_or_else(std::sync::PoisonError::into_inner) = syntax_id;
                    Ok(())
                });
                let _ = completed.send(outcome);
            });
        if spawn.is_err() {
            return;
        }
        if wait {
            let _ = receiver.recv_timeout(FETCH_WAIT);
        }
    }

    fn parse_and_publish(&self, bytes: &[u8]) -> Result<(), Error> {
        if bytes.is_empty() || bytes.len() > MAX_SYNTAX_BYTES {
            return Err(Error::Argument);
        }
        let OSD::Map(root) = OSDParser::deserialize_with_bytes(bytes.to_vec())? else {
            return Err(Error::Argument);
        };
        let parsed = parse_keywords(&root)?;
        if parsed.is_empty() {
            return Err(Error::Argument);
        }
        *keyword_store()
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = parsed;
        let handlers = self
            .events
            .handlers
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .iter()
            .map(|(_, handler)| Arc::clone(handler))
            .collect::<Vec<_>>();
        for handler in handlers {
            let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handler(())));
        }
        Ok(())
    }
}

fn fetch_bytes(client: &GridClient, uri: Uri) -> Result<Vec<u8>, Error> {
    let runtime = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .map_err(|_| Error::InvalidOperation)?;
    let http = client.native_http_caps_client();
    let (response, bytes) = runtime.block_on(http.get(uri, CancellationToken::default(), None))?;
    if !response.is_success_status_code() || bytes.is_empty() || bytes.len() > MAX_SYNTAX_BYTES {
        return Err(Error::InvalidOperation);
    }
    Ok(bytes)
}

fn cache_path(client: &GridClient, syntax_id: UUID) -> PathBuf {
    Path::new(&client.settings_ref().asset_cache.dir).join(format!("keywords_lsl_{syntax_id}.xml"))
}

fn persist_cache(path: &Path, bytes: &[u8]) -> Result<(), Error> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).map_err(|_| Error::InvalidOperation)?;
    }
    std::fs::write(path, bytes).map_err(|_| Error::InvalidOperation)
}

fn parse_keywords(
    root: &HashMap<String, OSD>,
) -> Result<HashMap<String, LslSyntaxLslKeyword>, Error> {
    if root
        .get(VERSION_KEY)
        .map(OSD::as_integer)
        .transpose()?
        .is_some_and(|version| version != 2)
    {
        return Err(Error::Argument);
    }
    let mut keywords = HashMap::new();
    let mut groups = root.iter().collect::<Vec<_>>();
    groups.sort_by(|left, right| left.0.cmp(right.0));
    for (group, value) in groups {
        if group == VERSION_KEY {
            continue;
        }
        let OSD::Map(items) = value else {
            continue;
        };
        let Some(category) = category(group) else {
            continue;
        };
        let mut items = items.iter().collect::<Vec<_>>();
        items.sort_by(|left, right| left.0.cmp(right.0));
        for (keyword, value) in items {
            let OSD::Map(attributes) = value else {
                continue;
            };
            let tooltip = tooltip(group, keyword, attributes)?;
            let value = LslSyntaxLslKeyword {
                category,
                keyword: keyword.clone(),
                tooltip,
                deprecated: boolean_attribute(attributes, "deprecated")?,
                god_mode: boolean_attribute(attributes, "god-mode")?,
            };
            if keywords.insert(keyword.clone(), value).is_some() {
                return Err(Error::Argument);
            }
        }
    }
    Ok(keywords)
}

fn category(group: &str) -> Option<LslSyntaxLslCategory> {
    match group {
        "controls" => Some(LslSyntaxLslCategory::Control),
        "types" => Some(LslSyntaxLslCategory::Datatype),
        "constants" => Some(LslSyntaxLslCategory::Constant),
        "events" => Some(LslSyntaxLslCategory::Event),
        "functions" => Some(LslSyntaxLslCategory::Function),
        _ => None,
    }
}

fn tooltip(group: &str, keyword: &str, attributes: &HashMap<String, OSD>) -> Result<String, Error> {
    let mut output = attributes
        .get("tooltip")
        .map(OSD::as_string)
        .transpose()?
        .unwrap_or_default();
    if attributes.contains_key("tooltip") {
        output.push('\n');
    }
    match group {
        "constants" => {
            let type_ = string_attribute(attributes, "type")?;
            let value = string_attribute(attributes, "value")?;
            output.push_str(&format!(" Type: {type_}-{value}"));
        }
        "events" => {
            output.push_str(&format!(
                "{keyword} ({})",
                parse_arguments(attributes.get("arguments"))?
            ));
        }
        "functions" => {
            let return_ = string_attribute(attributes, "return")?;
            output.push_str(&format!(
                "{return_} {keyword} ({})\n",
                parse_arguments(attributes.get("arguments"))?
            ));
            let energy = attributes
                .get("energy")
                .map(OSD::as_string)
                .transpose()?
                .unwrap_or_else(|| "0.0".to_owned());
            output.push_str(&format!("Energy: {energy}"));
            if let Some(sleep) = attributes.get("sleep") {
                output.push_str(&format!(", Sleep: {}", sleep.as_string()?));
            }
        }
        _ => {}
    }
    Ok(output)
}

fn parse_arguments(value: Option<&OSD>) -> Result<String, Error> {
    let Some(OSD::Array(arguments)) = value else {
        return Ok(String::new());
    };
    let mut rendered = Vec::new();
    for argument in arguments {
        let OSD::Map(argument) = argument else {
            continue;
        };
        let mut values = argument.iter().collect::<Vec<_>>();
        values.sort_by(|left, right| left.0.cmp(right.0));
        for (name, type_) in values {
            rendered.push(format!("{} {name}", type_.as_string()?));
        }
    }
    Ok(rendered.join(", "))
}

fn string_attribute(attributes: &HashMap<String, OSD>, name: &str) -> Result<String, Error> {
    attributes
        .get(name)
        .map(OSD::as_string)
        .transpose()
        .map(Option::unwrap_or_default)
        .map_err(Into::into)
}

fn boolean_attribute(attributes: &HashMap<String, OSD>, name: &str) -> Result<bool, Error> {
    attributes
        .get(name)
        .map(OSD::as_boolean)
        .transpose()
        .map(|value| value.unwrap_or(false))
        .map_err(Into::into)
}

#[cfg(test)]
mod tests {
    use std::sync::atomic::{AtomicUsize, Ordering};

    use super::*;

    static TEST_LOCK: Mutex<()> = Mutex::new(());

    #[test]
    fn embedded_catalog_is_complete_and_typed() {
        let _guard = TEST_LOCK.lock().unwrap();
        let _syntax = LslSyntax::new_with_constructor().unwrap();
        let keywords = LslSyntax::keywords().0;
        assert!(keywords.len() > 500);
        assert_eq!(
            keywords.get("llSay").unwrap().category,
            LslSyntaxLslCategory::Function
        );
        assert_eq!(
            keywords.get("state_entry").unwrap().category,
            LslSyntaxLslCategory::Event
        );
        assert_eq!(
            keywords.get("integer").unwrap().category,
            LslSyntaxLslCategory::Datatype
        );
    }

    #[test]
    fn parsed_catalog_replaces_snapshot_and_notifies_subscribers() {
        let _guard = TEST_LOCK.lock().unwrap();
        let syntax = LslSyntax::empty();
        let notifications = Arc::new(AtomicUsize::new(0));
        let observed = Arc::clone(&notifications);
        let _subscription = syntax.subscribe_syntax_changed(Arc::new(move |()| {
            observed.fetch_add(1, Ordering::Relaxed);
        }));
        let function = OSD::Map(HashMap::from([
            ("return".to_owned(), OSD::String("integer".to_owned())),
            ("energy".to_owned(), OSD::String("10.0".to_owned())),
            ("deprecated".to_owned(), OSD::Boolean(true)),
            (
                "arguments".to_owned(),
                OSD::Array(vec![OSD::Map(HashMap::from([(
                    "message".to_owned(),
                    OSD::String("string".to_owned()),
                )]))]),
            ),
        ]));
        let document = OSD::Map(HashMap::from([
            (VERSION_KEY.to_owned(), OSD::Integer(2)),
            (
                "functions".to_owned(),
                OSD::Map(HashMap::from([("osTest".to_owned(), function)])),
            ),
        ]));
        let bytes = OSDParser::serialize_llsd_xml_bytes(document).unwrap();
        syntax.0.parse_and_publish(&bytes).unwrap();

        assert_eq!(notifications.load(Ordering::Relaxed), 1);
        let keywords = LslSyntax::keywords().0;
        let keyword = keywords.get("osTest").unwrap();
        assert!(keyword.deprecated);
        assert_eq!(keyword.category, LslSyntaxLslCategory::Function);
        assert!(keyword.tooltip.contains("integer osTest (string message)"));
        assert!(keyword.tooltip.contains("Energy: 10.0"));
    }

    #[test]
    fn registration_on_opensim_without_syntax_capability_uses_default_catalog() {
        let _guard = TEST_LOCK.lock().unwrap();
        let client = GridClient::builder().build().unwrap();
        let syntax = LslSyntax::new_with_grid_client(client).unwrap();
        assert!(LslSyntax::keywords().0.contains_key("llOwnerSay"));
        assert_eq!(
            syntax
                .0
                .subscriptions
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .len(),
            1
        );
    }
}
