//! `users::` Rhai bridge — data-plane user management (v1.1.8).
//!
//! ```rhai
//! // CRUD
//! let u   = users::create(#{ email: "a@b", password: "hunter22a", display_name: "Alice" });
//! let u   = users::get(id);                      // map or ()
//! let u   = users::find_by_email("a@b");         // map or () — REQUIRES an
//!                                                //   authenticated principal
//!                                                //   (anti-enumeration); forbidden
//!                                                //   for anonymous public scripts.
//! let free = users::email_available("a@b");      // bool — anonymous-safe pre-check
//!                                                //   for self-serve registration.
//!                                                //   Leaks only "exists/doesn't" but
//!                                                //   is cheap + unthrottled: throttle
//!                                                //   the route if enumeration matters.
//! users::update(id, #{ display_name: "Alicia" });
//! let removed = users::delete(id);               // bool
//! let page = users::list(#{ "$limit": 50, cursor: () });
//!
//! // Auth
//! let tok = users::login("a@b", "hunter22a");   // session-token string or ()
//! let u   = users::verify(tok);                  // map or () (sliding-TTL bump)
//! users::logout(tok);
//!
//! // Email-tied (commit 5 / 6 / 7)
//! users::send_verification_email(id,
//!     #{ link_base: "https://app/verify", subject: "Confirm", body_template: "..." });
//! let u   = users::verify_email(token);
//! users::request_password_reset("a@b", #{...});
//! let u   = users::complete_password_reset(token, "new_pw");
//! users::invite("a@b",
//!     #{ link_base: "https://app/invite", subject: "Join", body_template: "...",
//!        display_name: "Bob", roles: ["editor"] });
//! let session_tok = users::accept_invite(token, "pw", "Bob");
//!
//! // Roles (commit 9)
//! users::add_role(id, "admin");
//! let removed = users::remove_role(id, "admin"); // bool
//! let yes = users::has_role(id, "admin");        // bool
//! ```
//!
//! Collection-less surface (mirrors `email::` / `secrets::` rather
//! than `kv::`/`docs::`/`files::`'s handle pattern). `app_id` is
//! derived from `cx.app_id` in the service — it never appears on the
//! script-side signature, preserving cross-app isolation.
//!
//! Methods bound but not yet implemented in the underlying service
//! return a `users::not_implemented` runtime error. Subsequent v1.1.8
//! commits flesh them out without re-touching this file.

use std::sync::Arc;

use super::bridge::{block_on, runtime_err};
use picloud_shared::{
    AppUser, AppUserId, CreateUserInput, EmailTemplateOpts, GeneratedAccept, GeneratedSession,
    InviteOpts, SdkCallCx, Services, UpdateUserInput, UsersListOpts, UsersListPage, UsersService,
};
use rhai::{Array, Dynamic, Engine as RhaiEngine, EvalAltResult, Map, Module};

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

    bind_create(&mut module, &svc, &cx);
    bind_get(&mut module, &svc, &cx);
    bind_find_by_email(&mut module, &svc, &cx);
    bind_email_available(&mut module, &svc, &cx);
    bind_update(&mut module, &svc, &cx);
    bind_delete(&mut module, &svc, &cx);
    bind_list(&mut module, &svc, &cx);
    bind_login(&mut module, &svc, &cx);
    bind_verify(&mut module, &svc, &cx);
    bind_logout(&mut module, &svc, &cx);
    bind_send_verification_email(&mut module, &svc, &cx);
    bind_verify_email(&mut module, &svc, &cx);
    bind_request_password_reset(&mut module, &svc, &cx);
    bind_complete_password_reset(&mut module, &svc, &cx);
    bind_invite(&mut module, &svc, &cx);
    bind_accept_invite(&mut module, &svc, &cx);
    bind_add_role(&mut module, &svc, &cx);
    bind_remove_role(&mut module, &svc, &cx);
    bind_has_role(&mut module, &svc, &cx);

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

// ----------------------------------------------------------------------------
// CRUD
// ----------------------------------------------------------------------------

fn bind_create(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "create",
        move |opts: Map| -> Result<Map, Box<EvalAltResult>> {
            let email = required_string(&opts, "email", "users::create")?;
            let password = required_string(&opts, "password", "users::create")?;
            let display_name = optional_string(&opts, "display_name");
            let svc = svc.clone();
            let cx = cx.clone();
            let user = block_on("users", async move {
                svc.create(
                    &cx,
                    CreateUserInput {
                        email,
                        password,
                        display_name,
                    },
                )
                .await
            })?;
            Ok(user_to_map(&user))
        },
    );
}

fn bind_get(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "get",
        move |id: &str| -> Result<Dynamic, Box<EvalAltResult>> {
            let id = parse_user_id(id, "users::get")?;
            let svc = svc.clone();
            let cx = cx.clone();
            let user_opt = block_on("users", async move { svc.get(&cx, id).await })?;
            Ok(user_opt.map_or(Dynamic::UNIT, |u| Dynamic::from(user_to_map(&u))))
        },
    );
}

fn bind_find_by_email(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "find_by_email",
        move |email: &str| -> Result<Dynamic, Box<EvalAltResult>> {
            let email = email.to_string();
            let svc = svc.clone();
            let cx = cx.clone();
            let user_opt = block_on("users", async move { svc.find_by_email(&cx, &email).await })?;
            Ok(user_opt.map_or(Dynamic::UNIT, |u| Dynamic::from(user_to_map(&u))))
        },
    );
}

/// `users::email_available(email)` → `bool`. Anonymous-safe pre-check for
/// self-serve registration (unlike `find_by_email`, which forbids
/// anonymous callers). Lets a public register script branch on a free vs
/// taken email without relying on `create`'s uniqueness error/502.
fn bind_email_available(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "email_available",
        move |email: &str| -> Result<bool, Box<EvalAltResult>> {
            let email = email.to_string();
            let svc = svc.clone();
            let cx = cx.clone();
            block_on(
                "users",
                async move { svc.email_available(&cx, &email).await },
            )
        },
    );
}

fn bind_update(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "update",
        move |id: &str, patch: Map| -> Result<Map, Box<EvalAltResult>> {
            let id = parse_user_id(id, "users::update")?;
            // display_name as Some(None) means "clear"; absent means
            // "leave alone". Differentiation: present-with-() vs absent.
            let display_name = if patch.contains_key("display_name") {
                Some(optional_string(&patch, "display_name"))
            } else {
                None
            };
            let patch = UpdateUserInput { display_name };
            let svc = svc.clone();
            let cx = cx.clone();
            let user = block_on("users", async move { svc.update(&cx, id, patch).await })?;
            Ok(user_to_map(&user))
        },
    );
}

fn bind_delete(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "delete",
        move |id: &str| -> Result<bool, Box<EvalAltResult>> {
            let id = parse_user_id(id, "users::delete")?;
            let svc = svc.clone();
            let cx = cx.clone();
            block_on("users", async move { svc.delete(&cx, id).await })
        },
    );
}

fn bind_list(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "list",
        move |opts: Map| -> Result<Map, Box<EvalAltResult>> {
            let limit = match opts.get("$limit").or_else(|| opts.get("limit")) {
                None => None,
                Some(d) if d.is_unit() => None,
                Some(d) => Some(
                    d.as_int()
                        .map_err(|_| runtime_err("users::list: '$limit' must be an integer"))?,
                ),
            };
            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("users::list: cursor must be a string or ()")),
            };
            let svc = svc.clone();
            let cx = cx.clone();
            let page: UsersListPage = block_on("users", async move {
                svc.list(&cx, UsersListOpts { cursor, limit }).await
            })?;
            Ok(list_page_to_map(&page))
        },
    );
}

// ----------------------------------------------------------------------------
// Auth
// ----------------------------------------------------------------------------

fn bind_login(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "login",
        move |email: &str, password: &str| -> Result<Dynamic, Box<EvalAltResult>> {
            let email = email.to_string();
            let password = password.to_string();
            let svc = svc.clone();
            let cx = cx.clone();
            let session_opt: Option<GeneratedSession> =
                block_on(
                    "users",
                    async move { svc.login(&cx, &email, &password).await },
                )?;
            Ok(session_opt.map_or(Dynamic::UNIT, |s| Dynamic::from(s.token)))
        },
    );
}

fn bind_verify(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "verify",
        move |token: &str| -> Result<Dynamic, Box<EvalAltResult>> {
            let token = token.to_string();
            let svc = svc.clone();
            let cx = cx.clone();
            let user_opt = block_on("users", async move { svc.verify(&cx, &token).await })?;
            Ok(user_opt.map_or(Dynamic::UNIT, |u| Dynamic::from(user_to_map(&u))))
        },
    );
}

fn bind_logout(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "logout",
        move |token: &str| -> Result<(), Box<EvalAltResult>> {
            let token = token.to_string();
            let svc = svc.clone();
            let cx = cx.clone();
            block_on("users", async move { svc.logout(&cx, &token).await })
        },
    );
}

// ----------------------------------------------------------------------------
// Email-tied (commit 5 / 6 / 7 fill the service impl behind these)
// ----------------------------------------------------------------------------

fn bind_send_verification_email(
    module: &mut Module,
    svc: &Arc<dyn UsersService>,
    cx: &Arc<SdkCallCx>,
) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "send_verification_email",
        move |id: &str, opts: Map| -> Result<(), Box<EvalAltResult>> {
            let id = parse_user_id(id, "users::send_verification_email")?;
            let opts = parse_email_template(&opts, "users::send_verification_email")?;
            let svc = svc.clone();
            let cx = cx.clone();
            block_on("users", async move {
                svc.send_verification_email(&cx, id, opts).await
            })
        },
    );
}

fn bind_verify_email(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "verify_email",
        move |token: &str| -> Result<Dynamic, Box<EvalAltResult>> {
            let token = token.to_string();
            let svc = svc.clone();
            let cx = cx.clone();
            let user_opt = block_on("users", async move { svc.verify_email(&cx, &token).await })?;
            Ok(user_opt.map_or(Dynamic::UNIT, |u| Dynamic::from(user_to_map(&u))))
        },
    );
}

fn bind_request_password_reset(
    module: &mut Module,
    svc: &Arc<dyn UsersService>,
    cx: &Arc<SdkCallCx>,
) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "request_password_reset",
        move |email: &str, opts: Map| -> Result<(), Box<EvalAltResult>> {
            let email = email.to_string();
            let opts = parse_email_template(&opts, "users::request_password_reset")?;
            let svc = svc.clone();
            let cx = cx.clone();
            block_on("users", async move {
                svc.request_password_reset(&cx, &email, opts).await
            })
        },
    );
}

fn bind_complete_password_reset(
    module: &mut Module,
    svc: &Arc<dyn UsersService>,
    cx: &Arc<SdkCallCx>,
) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "complete_password_reset",
        move |token: &str, new_password: &str| -> Result<Dynamic, Box<EvalAltResult>> {
            let token = token.to_string();
            let new_password = new_password.to_string();
            let svc = svc.clone();
            let cx = cx.clone();
            let user_opt = block_on("users", async move {
                svc.complete_password_reset(&cx, &token, &new_password)
                    .await
            })?;
            Ok(user_opt.map_or(Dynamic::UNIT, |u| Dynamic::from(user_to_map(&u))))
        },
    );
}

fn bind_invite(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "invite",
        move |email: &str, opts: Map| -> Result<(), Box<EvalAltResult>> {
            let email = email.to_string();
            let opts = parse_invite_opts(&opts)?;
            let svc = svc.clone();
            let cx = cx.clone();
            block_on("users", async move { svc.invite(&cx, &email, opts).await })
        },
    );
}

fn bind_accept_invite(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    // Two-arg overload: (token, password). The display_name overload is
    // bound separately because Rhai resolves functions by arity.
    {
        let svc = svc.clone();
        let cx = cx.clone();
        module.set_native_fn(
            "accept_invite",
            move |token: &str, password: &str| -> Result<Dynamic, Box<EvalAltResult>> {
                let token = token.to_string();
                let password = password.to_string();
                let svc = svc.clone();
                let cx = cx.clone();
                let accept_opt: Option<GeneratedAccept> = block_on("users", async move {
                    svc.accept_invite(&cx, &token, &password, None).await
                })?;
                Ok(accept_opt.map_or(Dynamic::UNIT, |a| Dynamic::from(a.session.token)))
            },
        );
    }
    {
        let svc = svc.clone();
        let cx = cx.clone();
        module.set_native_fn(
            "accept_invite",
            move |token: &str,
                  password: &str,
                  display_name: &str|
                  -> Result<Dynamic, Box<EvalAltResult>> {
                let token = token.to_string();
                let password = password.to_string();
                let display_name = if display_name.trim().is_empty() {
                    None
                } else {
                    Some(display_name.trim().to_string())
                };
                let svc = svc.clone();
                let cx = cx.clone();
                let accept_opt: Option<GeneratedAccept> = block_on("users", async move {
                    svc.accept_invite(&cx, &token, &password, display_name)
                        .await
                })?;
                Ok(accept_opt.map_or(Dynamic::UNIT, |a| Dynamic::from(a.session.token)))
            },
        );
    }
}

// ----------------------------------------------------------------------------
// Roles (commit 9 fills the service impl)
// ----------------------------------------------------------------------------

fn bind_add_role(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "add_role",
        move |id: &str, role: &str| -> Result<(), Box<EvalAltResult>> {
            let id = parse_user_id(id, "users::add_role")?;
            let role = role.to_string();
            let svc = svc.clone();
            let cx = cx.clone();
            block_on("users", async move { svc.add_role(&cx, id, &role).await })
        },
    );
}

fn bind_remove_role(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "remove_role",
        move |id: &str, role: &str| -> Result<bool, Box<EvalAltResult>> {
            let id = parse_user_id(id, "users::remove_role")?;
            let role = role.to_string();
            let svc = svc.clone();
            let cx = cx.clone();
            block_on(
                "users",
                async move { svc.remove_role(&cx, id, &role).await },
            )
        },
    );
}

fn bind_has_role(module: &mut Module, svc: &Arc<dyn UsersService>, cx: &Arc<SdkCallCx>) {
    let svc = svc.clone();
    let cx = cx.clone();
    module.set_native_fn(
        "has_role",
        move |id: &str, role: &str| -> Result<bool, Box<EvalAltResult>> {
            let id = parse_user_id(id, "users::has_role")?;
            let role = role.to_string();
            let svc = svc.clone();
            let cx = cx.clone();
            block_on("users", async move { svc.has_role(&cx, id, &role).await })
        },
    );
}

// ----------------------------------------------------------------------------
// Shape conversion helpers
// ----------------------------------------------------------------------------

fn user_to_map(user: &AppUser) -> Map {
    let mut m = Map::new();
    m.insert("id".into(), Dynamic::from(user.id.to_string()));
    m.insert("email".into(), Dynamic::from(user.email.clone()));
    m.insert(
        "display_name".into(),
        user.display_name
            .clone()
            .map_or(Dynamic::UNIT, Dynamic::from),
    );
    m.insert(
        "email_verified_at".into(),
        user.email_verified_at
            .map_or(Dynamic::UNIT, |d| Dynamic::from(d.to_rfc3339())),
    );
    m.insert(
        "last_login_at".into(),
        user.last_login_at
            .map_or(Dynamic::UNIT, |d| Dynamic::from(d.to_rfc3339())),
    );
    m.insert(
        "created_at".into(),
        Dynamic::from(user.created_at.to_rfc3339()),
    );
    m.insert(
        "updated_at".into(),
        Dynamic::from(user.updated_at.to_rfc3339()),
    );
    let roles: Array = user.roles.iter().cloned().map(Dynamic::from).collect();
    m.insert("roles".into(), roles.into());
    m
}

fn list_page_to_map(page: &UsersListPage) -> Map {
    let mut m = Map::new();
    let items: Array = page
        .items
        .iter()
        .map(|u| Dynamic::from(user_to_map(u)))
        .collect();
    m.insert("users".into(), items.into());
    m.insert(
        "next_cursor".into(),
        page.next_cursor
            .as_ref()
            .map_or(Dynamic::UNIT, |s| Dynamic::from(s.clone())),
    );
    m
}

fn parse_user_id(s: &str, ctx: &str) -> Result<AppUserId, Box<EvalAltResult>> {
    uuid::Uuid::parse_str(s)
        .map(AppUserId::from)
        .map_err(|e| runtime_err(&format!("{ctx}: id must be a UUID: {e}")))
}

fn parse_email_template(opts: &Map, ctx: &str) -> Result<EmailTemplateOpts, Box<EvalAltResult>> {
    Ok(EmailTemplateOpts {
        link_base: required_string(opts, "link_base", ctx)?,
        from: required_string(opts, "from", ctx)?,
        subject: required_string(opts, "subject", ctx)?,
        body_template: required_string(opts, "body_template", ctx)?,
    })
}

fn parse_invite_opts(opts: &Map) -> Result<InviteOpts, Box<EvalAltResult>> {
    // The template fields are optional only when invite is configured
    // to ship without an email; the service decides which is the
    // hard error. Here we forward whatever the script gave us.
    let template = if opts.contains_key("link_base")
        || opts.contains_key("subject")
        || opts.contains_key("body_template")
    {
        Some(parse_email_template(opts, "users::invite")?)
    } else {
        None
    };
    let display_name = optional_string(opts, "display_name");
    let roles = match opts.get("roles") {
        None => Vec::new(),
        Some(d) if d.is_unit() => Vec::new(),
        Some(d) => {
            if let Some(arr) = d.clone().try_cast::<Array>() {
                let mut out = Vec::with_capacity(arr.len());
                for el in arr {
                    if !el.is_string() {
                        return Err(runtime_err(
                            "users::invite: 'roles' must be an array of strings",
                        ));
                    }
                    out.push(el.into_string().unwrap_or_default());
                }
                out
            } else {
                return Err(runtime_err(
                    "users::invite: 'roles' must be an array of strings",
                ));
            }
        }
    };
    Ok(InviteOpts {
        template,
        display_name,
        roles,
    })
}

fn required_string(opts: &Map, key: &str, ctx: &str) -> Result<String, Box<EvalAltResult>> {
    match opts.get(key) {
        Some(d) if d.is_string() => Ok(d.clone().into_string().unwrap_or_default()),
        _ => Err(runtime_err(&format!(
            "{ctx}: '{key}' must be a string and is required"
        ))),
    }
}

fn optional_string(opts: &Map, key: &str) -> Option<String> {
    match opts.get(key) {
        None => None,
        Some(d) if d.is_unit() => None,
        Some(d) if d.is_string() => Some(d.clone().into_string().unwrap_or_default()),
        Some(d) => Some(d.to_string()),
    }
}
