//! Bund scripting integration.
//!
//! Inkhaven's foothold for Vladimir's Bund language — a stack-based
//! scripting layer (`bundcore` + `bund_language_parser` +
//! `rust_multistackvm`) intended to host user-authored hooks, custom
//! AI prompt templates, and save-time rules.
//!
//! ## Adam
//!
//! "Adam" is the canonical name for the process-wide singleton VM —
//! the first one, the one that already has stdlib loaded. The name
//! comes from bundcore itself (`BundVM::adam`). `init_adam()` is
//! called exactly once per process, lazily on the first `eval()`.
//!
//! ## Active store
//!
//! `ink.*` stdlib words (Phase 1) need access to the project's
//! `Store`. Inkhaven runs single-project-per-process, so we install
//! the store into a global `ACTIVE_STORE` slot once and read it out
//! of each word handler. CLI commands that don't open a project
//! (only `inkhaven bund` so far) simply leave the slot empty and
//! the ink words error gracefully.
//!
//! ## What's wired in each phase
//!
//! - **P0**: `init_adam()`, `eval()` — round-trip a Bund script.
//! - **P1** *(this file)*: `register_active_store()` + read-only
//!   `ink.*` stdlib words via `stdlib::register_ink_stdlib`.
//! - **P3**: sandbox policy.
//! - **P4**: hook points fired from `src/store/mod.rs`.
//! - **P5**: first-class `NodeKind::Script` + Bund-aware editor.
//! - **P6**: ephemeral worker pool + result queue.

pub mod hooks;
pub mod policy;
pub mod stdlib;

use std::cell::Cell;

thread_local! {
    /// Set while a `bund::eval` is in progress on this thread.
    /// `hooks::fire` checks this and short-circuits — otherwise
    /// any `ink.*` word that mutates the store (and so fires a
    /// hook) would re-enter `with_adam` and deadlock against
    /// the write lock the current eval is holding.
    static IN_BUND_EVAL: Cell<bool> = const { Cell::new(false) };

    /// Raw pointer to the live `App` set by `App::scripting_eval`
    /// for the duration of one Bund evaluation, then cleared.
    /// Stays null in CLI / non-TUI contexts — `ink.editor.*` /
    /// `ink.ai.*` / `ink.typst.*` words error with a clear
    /// "no active App" message when the slot is null.
    ///
    /// SAFETY contract:
    /// - The pointer is set from a `&mut self` on App; while it
    ///   is non-null, App is borrowed by the call that set it.
    /// - Bund word handlers cast back to `&mut App` via this
    ///   pointer. That creates a *second* mutable reference in
    ///   Rust's eyes — technically aliasing — but the TUI is
    ///   single-threaded and the access is strictly nested
    ///   inside the call that holds the outer borrow.
    /// - Always cleared by the RAII guard on drop, so panics +
    ///   early returns can't leak a stale pointer.
    static ACTIVE_APP: Cell<*mut crate::tui::app::App> = const {
        Cell::new(std::ptr::null_mut())
    };
}

/// True if the current thread is inside a `bund::eval`.
pub(crate) fn is_in_eval() -> bool {
    IN_BUND_EVAL.with(|c| c.get())
}

/// Run `f` with a mutable reference to the active `App`, if one
/// is installed. Returns `None` when called outside an
/// `App::scripting_eval` (e.g., from CLI `inkhaven bund` or from
/// a hook fired without an App in scope). Stdlib words use this
/// to reach App state for editor / AI / Typst mutation.
///
/// SAFETY: see the module-level doc-comment on `ACTIVE_APP`.
pub(crate) fn with_active_app<F, R>(f: F) -> Option<R>
where
    F: FnOnce(&mut crate::tui::app::App) -> R,
{
    let ptr = ACTIVE_APP.with(|c| c.get());
    if ptr.is_null() {
        return None;
    }
    // SAFETY: the pointer is non-null only between
    // App::scripting_eval entry (which sets the slot from
    // `self as *mut _`) and the matching RAII drop. The TUI is
    // single-threaded; nothing else can take a reference to
    // App during that window. The aliasing-with-the-outer-
    // borrow concern is documented in ACTIVE_APP's doc-comment.
    Some(f(unsafe { &mut *ptr }))
}

/// RAII guard for `ACTIVE_APP`. Constructed by
/// `App::scripting_eval`, restores the prior pointer on drop so
/// nested evals (we never nest in practice, but defensively)
/// don't leak the wrong reference.
pub(crate) struct AppGuard {
    prev: *mut crate::tui::app::App,
}

impl AppGuard {
    pub(crate) fn enter(app: &mut crate::tui::app::App) -> Self {
        let new: *mut crate::tui::app::App = app;
        let prev = ACTIVE_APP.with(|c| c.replace(new));
        Self { prev }
    }
}

impl Drop for AppGuard {
    fn drop(&mut self) {
        ACTIVE_APP.with(|c| c.set(self.prev));
    }
}

/// RAII guard that flips `IN_BUND_EVAL` true on construction and
/// resets to its prior value on drop. Used inside `eval` so a
/// panic still clears the flag and subsequent hooks fire
/// normally.
struct EvalGuard {
    prev: bool,
}

impl EvalGuard {
    fn enter() -> Self {
        let prev = IN_BUND_EVAL.with(|c| c.replace(true));
        Self { prev }
    }
}

impl Drop for EvalGuard {
    fn drop(&mut self) {
        IN_BUND_EVAL.with(|c| c.set(self.prev));
    }
}

use anyhow::{anyhow, Result};
use bundcore::bundcore::Bund;
use parking_lot::RwLock;
use rust_dynamic::value::Value;
use std::sync::OnceLock;

use crate::config::Config;
use crate::store::Store;
use policy::Policy;

/// Process-wide singleton Bund VM. Borrows the bundcore "Adam"
/// terminology — see module docs.
static ADAM: OnceLock<RwLock<Bund>> = OnceLock::new();

/// The project store, set once at startup (either when the TUI
/// opens a project or when the CLI's `bund` subcommand chooses to
/// expose the project). `None` is a valid state: scripts that try
/// to use `ink.*` words against a script-only invocation will see
/// a clean "no project store registered" error.
static ACTIVE_STORE: OnceLock<Store> = OnceLock::new();

/// Project Config, set alongside `ACTIVE_STORE`. Several
/// `Store::*` methods (`create_node`, snapshot semantics)
/// require it for hierarchy validation + artefact path
/// resolution.
static ACTIVE_CONFIG: OnceLock<Config> = OnceLock::new();

/// Sandbox policy to apply when Adam is built. Setters land before
/// the first `eval()` triggers lazy init; once Adam exists, the
/// policy is frozen for the process. `None` (no setter called) ⇒
/// the bundcore vanilla default, which deny destructive categories.
static POLICY: OnceLock<Policy> = OnceLock::new();

/// Initialise the Adam VM exactly once. Idempotent — subsequent
/// calls are no-ops. The order matters:
///
/// 1. `Bund::new()` loads bundcore's vanilla stdlib (arithmetic,
///    strings, conditionals, lambdas).
/// 2. `register_ink_stdlib` adds inkhaven's read-only `ink.*` words.
/// 3. `policy::apply_policy` re-registers denied words with a stub.
/// 4. The inline `scripting.bootstrap` HJSON script runs once.
/// 5. Every `NodeKind::Script` in the active store gets eval'd in
///    tree order — that's where most user-authored hook lambdas
///    actually live (P5).
///
/// Steps 4 and 5 are best-effort: a syntax error in a single
/// script logs a WARN and skips that script; the others still
/// run, and Adam still finishes constructing.
pub fn init_adam() -> Result<()> {
    if ADAM.get().is_some() {
        return Ok(());
    }
    let mut bund = Bund::new();
    stdlib::register_ink_stdlib(&mut bund.vm)
        .map_err(|e| anyhow!("register ink stdlib: {e}"))?;
    let p = POLICY.get().cloned().unwrap_or_default();
    if !p.is_open() {
        policy::apply_policy(&mut bund.vm, &p)
            .map_err(|e| anyhow!("apply policy: {e}"))?;
    }
    if !p.bootstrap.trim().is_empty() {
        if let Err(e) = bund.eval(p.bootstrap.clone()) {
            tracing::warn!(
                target: "inkhaven::scripting",
                "bootstrap script failed: {}",
                e
            );
        }
    }
    load_store_scripts(&mut bund);
    let _ = ADAM.set(RwLock::new(bund));
    Ok(())
}

/// Walk the active store for every `NodeKind::Script` node, read
/// its body, and `bund.eval` it against the supplied VM. Errors
/// per-script are logged and continue; no script can break the
/// rest of init.
///
/// No-ops cleanly when no project store is registered (e.g., a
/// pure `inkhaven bund "40 2 +"` invocation outside any project).
fn load_store_scripts(bund: &mut Bund) {
    let Some(store) = active_store() else { return };
    let hierarchy = match crate::store::hierarchy::Hierarchy::load(store) {
        Ok(h) => h,
        Err(e) => {
            tracing::warn!(
                target: "inkhaven::scripting",
                "load_store_scripts: hierarchy load failed: {}",
                e
            );
            return;
        }
    };
    for node in hierarchy.iter() {
        if node.kind != crate::store::NodeKind::Script {
            continue;
        }
        let bytes = match store.get_content(node.id) {
            Ok(Some(b)) => b,
            Ok(None) => continue,
            Err(e) => {
                tracing::warn!(
                    target: "inkhaven::scripting",
                    "script {} read failed: {}",
                    node.id,
                    e
                );
                continue;
            }
        };
        let body = String::from_utf8_lossy(&bytes).into_owned();
        if body.trim().is_empty() {
            continue;
        }
        if let Err(e) = bund.eval(body) {
            tracing::warn!(
                target: "inkhaven::scripting",
                "script `{}` ({}) eval failed: {}",
                node.title,
                node.id,
                e
            );
        }
    }
}

/// Run `f` with a mutable reference to Adam. Returns `None` when
/// Adam hasn't been built yet — callers handle that as "no script
/// runtime available, skip". Used internally by `hooks::fire`.
pub(crate) fn with_adam<F, R>(f: F) -> Option<R>
where
    F: FnOnce(&mut Bund) -> R,
{
    let adam = ADAM.get()?;
    let mut guard = adam.write();
    Some(f(&mut guard))
}

/// Install the sandbox policy. Must be called BEFORE the first
/// `eval()` — otherwise Adam is already constructed under the
/// default policy and the call is a no-op. Idempotent in practice
/// (single-project-per-process).
pub fn set_policy(policy: Policy) {
    let _ = POLICY.set(policy);
}

/// Install the project store into the global slot. Called by the
/// TUI startup path and by the CLI when a subcommand wants its
/// Bund expressions to see the project. Idempotent in practice —
/// subsequent calls silently no-op (single-project-per-process).
pub fn register_active_store(store: Store) {
    let _ = ACTIVE_STORE.set(store);
}

/// Read access to the active project config. Used by `ink.tree.*`
/// write words that need `cfg` for hierarchy validation.
pub fn active_config() -> Option<&'static Config> {
    ACTIVE_CONFIG.get()
}

/// Install the project config into the global slot. Called from
/// `Store::open` via `configure`.
pub fn register_active_config(cfg: Config) {
    let _ = ACTIVE_CONFIG.set(cfg);
}

/// One-shot helper called from `Store::open` so every code path
/// that opens a project — TUI, `inkhaven bund`, `inkhaven add`,
/// `inkhaven reindex`, etc. — automatically arms the scripting
/// layer. Installs policy + store + config together.
///
/// All three inner calls are idempotent (single-project-per-
/// process), so a second open against the same project is harmless.
pub fn configure(policy: Policy, store: Store, cfg: Config) {
    set_policy(policy);
    register_active_store(store);
    register_active_config(cfg);
}

/// Read access to the active store, used by `ink.*` word handlers.
/// `None` means no project has been opened in this process.
pub fn active_store() -> Option<&'static Store> {
    ACTIVE_STORE.get()
}

/// Result of an `eval` call: any text written to bundcore's
/// (overridden) `print` / `println` stream, plus whatever value
/// remained on the top of the workbench when the script finished.
///
/// Both fields are optional in spirit:
///   * `stdout` is "" when nothing printed
///   * `top` is `None` when the stack was empty after the script
///
/// Callers decide how to surface them — CLI bund prints stdout to
/// the real terminal and then the top; TUI Ctrl+Z E concatenates
/// them on the status bar.
#[derive(Debug, Default)]
pub struct EvalOutput {
    pub stdout: String,
    pub top: Option<Value>,
}

/// Parse + evaluate `code` against Adam. Returns an `EvalOutput`
/// containing the captured print buffer and the top of the
/// workbench. Auto-initialises Adam on the first call.
pub fn eval(code: &str) -> Result<EvalOutput> {
    init_adam()?;
    let adam = ADAM.get().ok_or_else(|| anyhow!("Adam VM missing after init"))?;
    let _ = stdlib::io::drain_print_buffer();
    // Mark this thread as "inside eval" so hook fires that
    // originate from Store mutations the script triggers (via
    // `ink.tree.*`, `ink.paragraph.*`, etc.) short-circuit
    // instead of re-entering the write lock we're about to take.
    let _eval_guard = EvalGuard::enter();
    let mut guard = adam.write();
    guard
        .eval(code)
        .map_err(|e| anyhow!("bund eval failed: {e}"))?;
    let top = guard.vm.stack.pull();
    drop(guard);
    let stdout = stdlib::io::drain_print_buffer();
    Ok(EvalOutput { stdout, top })
}

/// Render a `rust_dynamic::Value` as a human-readable string. Used
/// by the CLI subcommand and (eventually) the TUI command output.
///
/// Strategy: scalar variants (string/int/float/bool) render as
/// their bare value so `inkhaven bund "40 2 +"` prints `42` rather
/// than `Value { … }`. Compound variants (list, map) go through
/// rust_dynamic's `cast_value_to_json` and get pretty-printed —
/// suitable for piping into `jq` or eyeballing the structure.
pub fn format_value(v: &Value) -> String {
    if let Ok(s) = v.clone().cast_string() {
        return s;
    }
    if let Ok(i) = v.clone().cast_int() {
        return i.to_string();
    }
    if let Ok(f) = v.clone().cast_float() {
        return f.to_string();
    }
    if let Ok(b) = v.clone().cast_bool() {
        return b.to_string();
    }
    let j = value_to_json(v);
    serde_json::to_string_pretty(&j).unwrap_or_else(|_| j.to_string())
}

/// Recursive `Value` → `serde_json::Value` converter that fills the
/// gap in `rust_dynamic::Value::cast_value_to_json`: the upstream
/// helper doesn't handle the STRING variant (it errors at the
/// `_ =>` arm), so a list-of-maps-of-strings — which is exactly
/// what every `ink.*` word returns — comes out as Debug noise.
/// This walks the value ourselves and falls through to a debug
/// stringification only for variants we don't recognise.
fn value_to_json(v: &Value) -> serde_json::Value {
    if let Ok(s) = v.clone().cast_string() {
        return serde_json::Value::String(s);
    }
    if let Ok(i) = v.clone().cast_int() {
        return serde_json::Value::from(i);
    }
    if let Ok(f) = v.clone().cast_float() {
        return serde_json::Value::from(f);
    }
    if let Ok(b) = v.clone().cast_bool() {
        return serde_json::Value::Bool(b);
    }
    if let Ok(list) = v.clone().cast_list() {
        return serde_json::Value::Array(list.iter().map(value_to_json).collect());
    }
    if let Ok(dict) = v.clone().cast_dict() {
        let mut m = serde_json::Map::new();
        for (k, val) in dict.iter() {
            m.insert(k.clone(), value_to_json(val));
        }
        return serde_json::Value::Object(m);
    }
    // Last resort: NONE, NODATA, unrecognised variants.
    serde_json::Value::Null
}
