//! Shared Rhai scripting infrastructure.
//!
//! Both the server and client embed a Rhai engine. This module provides
//! the shared command dispatch logic and the client-side [`ScriptHost`].
//!
//! Scripts are loaded from the `scripts/` directory at runtime, resolved
//! relative to the current working directory.
//!
//! ## Client native functions
//!
//! | Function | Returns | Description |
//! |---|---|---|
//! | `set_name(name)` | `String` | Set display name, returns confirmation |
//! | `get_name()` | `String` | Get current display name |
//! | `send_chat(text)` | `()` | Send a chat message |
//! | `send_msg(text)` | `()` | Alias for `send_chat` |

use std::sync::{Arc, Mutex};

use rhai::{AST, Dynamic, Engine, EvalAltResult, Scope};
use tokio::sync::mpsc::UnboundedSender;

use crate::core::protocol::ClientMessage;

/// Load and compile a `.rhai` script from `<data_dir>/scripts/<name>.rhai`.
///
/// Returns `None` if the file doesn't exist or fails to compile.
pub fn load_script(engine: &Engine, name: &str) -> Option<AST> {
    let path = crate::core::paths::data_path(&format!("scripts/{name}.rhai"));
    if !path.exists() {
        eprintln!("Warning: script not found: {}", path.display());
        return None;
    }
    match engine.compile_file(path.clone()) {
        Ok(ast) => Some(ast),
        Err(e) => {
            eprintln!("Error compiling {}: {e}", path.display());
            None
        }
    }
}

/// Dispatch a `/command args` string to a `cmd_{command}(args)` Rhai function.
///
/// Returns the string result if the function returns one, or `None` otherwise.
pub fn dispatch_command(
    engine: &Engine,
    ast: &AST,
    input: &str,
) -> Result<Option<String>, Box<EvalAltResult>> {
    let input = input.strip_prefix('/').unwrap_or(input);
    let (cmd, args) = match input.split_once(' ') {
        Some((cmd, args)) => (cmd, args.trim().to_string()),
        None => (input, String::new()),
    };

    let fn_name = format!("cmd_{cmd}");
    let mut scope = Scope::new();

    let result: Dynamic = engine.call_fn(&mut scope, ast, &fn_name, (args,))?;

    if result.is_string() {
        Ok(Some(result.into_string().unwrap()))
    } else {
        Ok(None)
    }
}

/// Client-side Rhai scripting host.
///
/// Exposes native functions for interacting with the server and tracks
/// the player's current display name.
pub struct ScriptHost {
    engine: Engine,
    ast: AST,
}

impl ScriptHost {
    pub fn new(tx: UnboundedSender<ClientMessage>) -> Self {
        let mut engine = Engine::new();
        let name: Arc<Mutex<String>> = Arc::new(Mutex::new(String::from("unnamed")));

        let name_ref = name.clone();
        let tx_ref = tx.clone();
        engine.register_fn("set_name", move |new_name: &str| -> String {
            *name_ref.lock().unwrap() = new_name.to_string();
            let _ = tx_ref.send(ClientMessage::SetName {
                name: new_name.to_string(),
            });
            format!("Name set to: {new_name}")
        });

        let name_ref = name.clone();
        engine.register_fn("get_name", move || -> String {
            name_ref.lock().unwrap().clone()
        });

        let tx_ref = tx.clone();
        engine.register_fn("send_chat", move |content: &str| {
            let _ = tx_ref.send(ClientMessage::Chat {
                content: content.to_string(),
            });
        });

        let tx_ref = tx.clone();
        engine.register_fn("send_msg", move |content: &str| {
            let _ = tx_ref.send(ClientMessage::Chat {
                content: content.to_string(),
            });
        });

        let ast = load_script(&engine, "client").expect("failed to load scripts/client.rhai");

        Self { engine, ast }
    }

    pub fn handle_command(&self, input: &str) -> Result<Option<String>, Box<EvalAltResult>> {
        dispatch_command(&self.engine, &self.ast, input)
    }
}
