//! JSON ↔ Rhai `Dynamic` value bridge.
//!
//! Originally inline in `engine.rs`; moved here for v1.1.0 so future
//! service modules (KV in v1.1.1, docs in v1.1.2, …) can convert
//! values without `engine.rs` being the only owner of the conversions.
//! Behaviour is unchanged from the pre-extraction implementation —
//! `sdk_contract.rs::json_round_trip_preserves_nested_shapes` pins the
//! observable round-trip.

use rhai::{Dynamic, EvalAltResult, Map};
use serde_json::Value as Json;
use tokio::runtime::Handle as TokioHandle;

/// Run an async future inside the synchronous Rhai context.
///
/// `LocalExecutorClient` wraps script execution in `spawn_blocking`, so
/// the current Tokio runtime is reachable via `Handle::current()`. We
/// block on it directly; we are NOT calling this from an async task,
/// so blocking is the correct primitive.
///
/// Prefix each error string with `service` so a script reading the
/// runtime error message learns which SDK surface threw it.
///
/// # Errors
///
/// Wraps the future's error variant or "no tokio runtime available" in
/// `EvalAltResult::ErrorRuntime`, suitable to return from a Rhai-
/// registered fn.
pub fn block_on<T, E, F>(service: &str, fut: F) -> Result<T, Box<EvalAltResult>>
where
    F: std::future::Future<Output = Result<T, E>>,
    E: std::fmt::Display,
{
    let handle = TokioHandle::try_current().map_err(|e| -> Box<EvalAltResult> {
        EvalAltResult::ErrorRuntime(
            format!("{service}: no tokio runtime available: {e}").into(),
            rhai::Position::NONE,
        )
        .into()
    })?;
    handle.block_on(fut).map_err(|err| -> Box<EvalAltResult> {
        EvalAltResult::ErrorRuntime(format!("{service}: {err}").into(), rhai::Position::NONE).into()
    })
}

/// Convert a `serde_json::Value` into a Rhai `Dynamic` suitable for
/// pushing into a script's scope. Numbers prefer the narrowest type
/// (`i64` over `f64`); anything that can't round-trip falls back to a
/// string so the script always sees a defined value.
pub fn json_to_dynamic(value: Json) -> Dynamic {
    match value {
        Json::Null => Dynamic::UNIT,
        Json::Bool(b) => b.into(),
        Json::Number(n) => {
            if let Some(i) = n.as_i64() {
                i.into()
            } else if let Some(f) = n.as_f64() {
                f.into()
            } else {
                n.to_string().into()
            }
        }
        Json::String(s) => s.into(),
        Json::Array(arr) => arr
            .into_iter()
            .map(json_to_dynamic)
            .collect::<Vec<Dynamic>>()
            .into(),
        Json::Object(obj) => {
            let mut m = Map::new();
            for (k, v) in obj {
                m.insert(k.into(), json_to_dynamic(v));
            }
            Dynamic::from(m)
        }
    }
}

/// Convert a Rhai `Dynamic` back to a `serde_json::Value`. Custom Rhai
/// types (timestamps, user-registered modules) fall back to their
/// `Display` form so they appear as strings in JSON output rather than
/// failing the response build.
pub fn dynamic_to_json(value: &Dynamic) -> Json {
    if value.is_unit() {
        return Json::Null;
    }
    if let Ok(b) = value.as_bool() {
        return Json::Bool(b);
    }
    if let Ok(i) = value.as_int() {
        return Json::Number(i.into());
    }
    if let Ok(f) = value.as_float() {
        return serde_json::Number::from_f64(f).map_or(Json::Null, Json::Number);
    }
    if value.is_string() {
        return Json::String(value.clone().into_string().unwrap_or_default());
    }
    if let Some(arr) = value.clone().try_cast::<rhai::Array>() {
        return Json::Array(arr.iter().map(dynamic_to_json).collect());
    }
    if let Some(map) = value.clone().try_cast::<Map>() {
        let mut out = serde_json::Map::new();
        for (k, v) in map {
            out.insert(k.to_string(), dynamic_to_json(&v));
        }
        return Json::Object(out);
    }
    Json::String(value.to_string())
}
