//! `secrets::` Rhai bridge — encrypted per-app secrets (v1.1.7).
//!
//! ```rhai
//! secrets::set("stripe_key", "sk_live_xxx");
//! secrets::set("oauth", #{ client_id: "abc", client_secret: "xyz" });
//! let key = secrets::get("stripe_key");          // value or ()
//! let removed = secrets::delete("stripe_key");    // bool
//! let page = secrets::list(#{ cursor: (), limit: 100 });
//! //   page = #{ names: [...], next_cursor: () | "..." }
//! ```
//!
//! Collection-less (secrets are per-app, like pubsub topics) so there's
//! no `::collection(...)`. Values are any JSON-serializable Rhai value
//! (String/Map/Array/number/bool); a String round-trips back as a
//! String. `app_id` is derived from `cx.app_id` in the service — it
//! never appears in the script-side signature, preserving cross-app
//! isolation.

use std::sync::Arc;

use picloud_shared::{SdkCallCx, SecretsListPage, Services};
use rhai::{Array, Dynamic, Engine as RhaiEngine, EvalAltResult, Map, Module};

use super::bridge::{
    block_on, dynamic_to_json_capped, json_to_dynamic, runtime_err, MAX_JSON_MATERIALIZE_BYTES,
};

pub(super) fn register(engine: &mut RhaiEngine, services: &Services, cx: Arc<SdkCallCx>) {
    let svc = services.secrets.clone();
    let mut module = Module::new();

    // secrets::set(name, value) — overwrites if present.
    {
        let svc = svc.clone();
        let cx = cx.clone();
        module.set_native_fn(
            "set",
            move |name: &str, value: Dynamic| -> Result<(), Box<EvalAltResult>> {
                let json = dynamic_to_json_capped(&value, MAX_JSON_MATERIALIZE_BYTES)?;
                let svc = svc.clone();
                let cx = cx.clone();
                block_on("secrets", async move { svc.set(&cx, name, json).await })
            },
        );
    }

    // secrets::get(name) — decoded value, or () if missing.
    {
        let svc = svc.clone();
        let cx = cx.clone();
        module.set_native_fn(
            "get",
            move |name: &str| -> Result<Dynamic, Box<EvalAltResult>> {
                let svc = svc.clone();
                let cx = cx.clone();
                let opt = block_on("secrets", async move { svc.get(&cx, name).await })?;
                Ok(opt.map_or(Dynamic::UNIT, json_to_dynamic))
            },
        );
    }

    // secrets::delete(name) — bool was-present.
    {
        let svc = svc.clone();
        let cx = cx.clone();
        module.set_native_fn(
            "delete",
            move |name: &str| -> Result<bool, Box<EvalAltResult>> {
                let svc = svc.clone();
                let cx = cx.clone();
                block_on("secrets", async move { svc.delete(&cx, name).await })
            },
        );
    }

    // secrets::list(#{ cursor, limit }) — names only, cursor-paginated.
    {
        let svc = svc.clone();
        let cx = cx.clone();
        module.set_native_fn(
            "list",
            move |opts: Map| -> Result<Map, Box<EvalAltResult>> {
                let (cursor, limit) = parse_list_opts(&opts)?;
                let svc = svc.clone();
                let cx = cx.clone();
                let page: SecretsListPage = block_on("secrets", async move {
                    svc.list(&cx, cursor.as_deref(), limit).await
                })?;
                Ok(list_page_to_map(page))
            },
        );
    }

    engine.register_static_module("secrets", module.into());
}

/// Pull `cursor` (string or `()`) and `limit` (int or `()`) out of the
/// options map. Unknown/extra keys are ignored.
fn parse_list_opts(opts: &Map) -> Result<(Option<String>, u32), Box<EvalAltResult>> {
    let cursor = match opts.get("cursor") {
        None => None,
        Some(d) if d.is_unit() => None,
        Some(d) if d.is_string() => Some(d.clone().into_string().unwrap_or_default()),
        Some(_) => return Err(runtime_err("secrets::list: cursor must be a string or ()")),
    };
    let limit = match opts.get("limit") {
        None => 0,
        Some(d) if d.is_unit() => 0,
        Some(d) => {
            let n = d
                .as_int()
                .map_err(|_| runtime_err("secrets::list: limit must be an integer or ()"))?;
            u32::try_from(n.max(0)).unwrap_or(u32::MAX)
        }
    };
    Ok((cursor, limit))
}

fn list_page_to_map(page: SecretsListPage) -> Map {
    let mut m = Map::new();
    let names: Array = page.names.into_iter().map(Dynamic::from).collect();
    m.insert("names".into(), names.into());
    m.insert(
        "next_cursor".into(),
        page.next_cursor.map_or(Dynamic::UNIT, Dynamic::from),
    );
    m
}
