//! `url::` — RFC 3986 percent-encoding.
//!
//! `encode`/`decode` operate on opaque component values; `encode_query`
//! builds an `application/x-www-form-urlencoded`-style query string
//! from a Rhai `Map`. Key ordering is the map's natural order (Rhai's
//! `Map` is a `BTreeMap`, so keys come out alphabetically — fine for
//! query strings, which RFC 3986 leaves unordered).

use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use rhai::{Engine as RhaiEngine, EvalAltResult, Map, Module};

/// RFC 3986 unreserved set: `A-Z / a-z / 0-9 / - / _ / . / ~`.
/// Everything outside this set gets percent-encoded.
const UNRESERVED: &AsciiSet = &NON_ALPHANUMERIC
    .remove(b'-')
    .remove(b'_')
    .remove(b'.')
    .remove(b'~');

pub fn register(engine: &mut RhaiEngine) {
    let mut module = Module::new();
    register_encode(&mut module);
    register_decode(&mut module);
    register_encode_query(&mut module);
    engine.register_static_module("url", module.into());
}

fn register_encode(module: &mut Module) {
    module.set_native_fn("encode", |s: &str| -> Result<String, Box<EvalAltResult>> {
        Ok(utf8_percent_encode(s, UNRESERVED).to_string())
    });
}

fn register_decode(module: &mut Module) {
    module.set_native_fn("decode", |s: &str| -> Result<String, Box<EvalAltResult>> {
        percent_decode_str(s)
            .decode_utf8()
            .map(std::borrow::Cow::into_owned)
            .map_err(|e| format!("url::decode: invalid UTF-8: {e}").into())
    });
}

fn register_encode_query(module: &mut Module) {
    module.set_native_fn(
        "encode_query",
        |m: Map| -> Result<String, Box<EvalAltResult>> {
            let mut out = String::new();
            for (k, v) in m {
                if !out.is_empty() {
                    out.push('&');
                }
                out.push_str(&utf8_percent_encode(&k, UNRESERVED).to_string());
                out.push('=');
                // Coerce values via `to_string` rather than throwing on
                // non-strings — scripts commonly pass numbers/bools here
                // and a forced cast at the call site is friction with
                // no upside.
                let value = v.to_string();
                out.push_str(&utf8_percent_encode(&value, UNRESERVED).to_string());
            }
            Ok(out)
        },
    );
}
