// User-defined functions, aggregates, window functions and collations
// (Deliverable 06). See src/function.h for the round-trip design.
//
// Threading model, restated:
//
//  - sqlite invokes xFunc/xStep/xFinal/xValue/xInverse/xCollation on
//    whatever thread steps the statement: a libuv worker for everything
//    the async API runs, the JS thread for the *Sync methods.
//  - On a worker, the callback marshals its arguments into Cells and
//    makes a blocking round trip through the per-database
//    ThreadSafeFunction: the JS thread converts, runs the user's JS,
//    marshals the result back and signals a condition variable.
//  - On the JS thread (Database::sync_sqlite_depth > 0), the JS thread is
//    the one executing SQL, so the implementation is invoked directly —
//    re-entrantly, same-thread, like node:sqlite (Phase 2; see
//    RunDirectOnJsThread). Collations and the JS progress callback keep
//    refusing: they have no error channel, and the sync gate blocks them
//    up front.
//
// Lifetime answers (the checklist items this file must have settled):
//
//  - JsFunc holders own Napi references but are only ever read, mutated
//    and destroyed on the JS thread. xDestroy (JsFuncDestroy) fires from
//    sqlite3_create_function_v2 / sqlite3_close / unregister calls, all of
//    which this codebase makes from the JS thread (registration handlers,
//    RemoveUserFunctions in Work_BeginClose and ~Database — Work_Close
//    runs only after Work_BeginClose already unregistered everything, and
//    the failed-close path keeps the survivors precisely so no dangling
//    registration outlives its holder).
//  - Registrations are scheduled exclusively, i.e. they run only when
//    nothing is in flight (pending == 0). That is what makes the above
//    safe (no round trip can be executing while a holder is destroyed)
//    and what keeps the registration's sqlite3_create_function_v2 from
//    blocking the JS thread on the connection mutex a worker holds while
//    blocked mid-round-trip.
//  - The channel (one ThreadSafeFunction per database) is Unref'd at
//    creation so it never keeps the event loop alive, and Released once
//    the last registration is gone. Queued items — only ever aggregate
//    cleanups — are still delivered after Release, and at environment
//    teardown they may be dropped; that leaks the (small) AggState whose
//    Napi reference the environment reclaims anyway. Bounded, and
//    reachable only at teardown.
//  - FuncCall ownership: the worker allocates; for waited calls it also
//    frees (after the wake); fire-and-forget cleanups are freed by the JS
//    thread. Direct re-entrant calls are stack-disciplined: allocated and
//    freed by the same sqlite callback frame, never crossing threads.

#include <cstring>
#include <string>
#include <vector>

#include <sqlite3.h>
#include <napi.h>
#include <uv.h>

#include "macros.h"
#include "convert.h"
#include "database.h"
#include "function.h"

using namespace node_sqlite3;

namespace {

std::string RemovedMidFlightMessage(const std::string& name) {
    return "user-defined function '" + name + "' was removed while a call "
        "to it was still in flight";
}

// Human-readable JS type name for comparator/return validation errors.
std::string JsTypeName(const Napi::Value& value) {
    switch (value.Type()) {
        case napi_null:      return "null";
        case napi_undefined: return "undefined";
        case napi_boolean:   return "boolean";
        case napi_number:    return "number";
        case napi_string:    return "string";
        case napi_symbol:    return "symbol";
        case napi_object:    return "object";
        case napi_function:  return "function";
        case napi_bigint:    return "bigint";
        default:             return "value";
    }
}

// Extracts an error message from a thrown JS value without ever leaving a
// pending exception behind (the message getter of a hostile object can
// throw; anything pending after the reads is cleared and ignored).
std::string JsErrorMessage(Napi::Env env, Napi::Value err) {
    if (err.IsObject()) {
        Napi::Value msg = err.As<Napi::Object>().Get("message");
        if (!env.IsExceptionPending() && msg.IsString()) {
            return msg.As<Napi::String>().Utf8Value();
        }
        napi_value unused = NULL;
        napi_get_and_clear_last_exception(env, &unused);
    }
    if (err.IsString()) return err.As<Napi::String>().Utf8Value();
    return "non-error value thrown";
}

void DisposeFuncCall(FuncCall* call) {
    uv_mutex_destroy(&call->mutex);
    uv_cond_destroy(&call->cond);
    delete call;
}

// Hands the call to the JS thread and blocks until it completes. Returns
// false (with the call still owned by the caller) when the channel cannot
// accept it — the environment is shutting down.
void ApplyCell(sqlite3_context* ctx, const Cell& cell) {
    switch (cell.type) {
        case SQLITE_INTEGER:
            sqlite3_result_int64(ctx, cell.integer);
            break;
        case SQLITE_FLOAT:
            sqlite3_result_double(ctx, cell.real);
            break;
        case SQLITE_TEXT:
            sqlite3_result_text(ctx, cell.str.data(),
                static_cast<int>(cell.str.size()), SQLITE_TRANSIENT);
            break;
        case SQLITE_BLOB:
            sqlite3_result_blob(ctx,
                cell.str.empty() ? "" : cell.str.data(),
                static_cast<int>(cell.str.size()), SQLITE_TRANSIENT);
            break;
        default:
            sqlite3_result_null(ctx);
    }
}

// Reports the round trip's outcome to sqlite and frees the call. Steps and
// inverses produce no result value; their only outcome is the error.
void FinishCall(sqlite3_context* ctx, FuncCall* call, bool has_result) {
    if (call->errored) {
        sqlite3_result_error(ctx, call->error.c_str(), -1);
    }
    else if (has_result) {
        ApplyCell(ctx, call->result);
    }
    DisposeFuncCall(call);
}

} // namespace

// Free functions that need Database's protected state. Everything the
// user-function machinery does to a Database outside its member functions
// lives here, so the friendship grant is one line and auditable.
namespace node_sqlite3 {

struct UserFunctionOps {
static bool RunBlockingRoundTrip(FuncCall* call) {
    napi_status st = napi_call_threadsafe_function(call->db->js_channel,
        call, napi_tsfn_blocking);
    if (st != napi_ok) {
        call->errored = true;
        call->error = "the JavaScript environment is shutting down; "
            "user-defined function '" + call->fn->name + "' cannot run";
        return false;
    }
    uv_mutex_lock(&call->mutex);
    while (!call->done) uv_cond_wait(&call->cond, &call->mutex);
    uv_mutex_unlock(&call->mutex);
    return true;
}
// Fire-and-forget variant used for aggregate-state cleanup from contexts
// that must not (or need not) wait: the JS-thread finalize paths.
static void EnqueueAggCleanup(Database* db, JsFunc* fn, AggState* agg) {
    FuncCall* call = new FuncCall(fn, FuncCall::kAggCleanup);
    call->agg = agg;
    call->fire_and_forget = true;
    napi_status st = napi_call_threadsafe_function(db->js_channel,
        call, napi_tsfn_nonblocking);
    if (st != napi_ok) {
        // Environment shutting down: the reference inside is reclaimed at
        // env teardown; freeing the struct here is all we can do. (The
        // Napi::Reference destructor needs the JS thread.)
        delete call;
    }
}
// --- The JS-thread half ----------------------------------------------------

// Records a JS exception (or a converter TypeError/RangeError) as the
// call's error, keeping the thrown value on the database as the `cause` of
// the step failure it is about to cause.
static void SetCallError(Database* db, FuncCall* call, const std::string& prefix) {
    Napi::Env env(db->Env());
    napi_value pending = NULL;
    napi_get_and_clear_last_exception(env, &pending);
    call->errored = true;
    if (pending == NULL) {
        call->error = prefix + "unknown error";
        return;
    }
    Napi::Value err(env, pending);
    call->error = prefix + JsErrorMessage(env, err);
    db->pending_js_error = Napi::Persistent(err);
}

// Converts a JS return value into the call's result Cell via the shared
// bind converter. A pending exception afterwards means ConvertToField
// rejected the value — its message already names the return value.
static void ConvertReturnValue(Database* db, FuncCall* call, Napi::Value value,
        const std::string& name) {
    auto field = ConvertToField(value,
        "the return value of function '" + name + "'");
    if (field == nullptr) {
        SetCallError(db, call, "");
        return;
    }
    switch (field->type) {
        case SQLITE_INTEGER: {
            call->result.type = SQLITE_INTEGER;
            call->result.integer =
                static_cast<Values::Integer*>(field.get())->value;
        } break;
        case SQLITE_FLOAT: {
            call->result.type = SQLITE_FLOAT;
            call->result.real =
                static_cast<Values::Float*>(field.get())->value;
        } break;
        case SQLITE_TEXT: {
            call->result.type = SQLITE_TEXT;
            call->result.str =
                std::move(static_cast<Values::Text*>(field.get())->value);
        } break;
        case SQLITE_BLOB: {
            auto* f = static_cast<Values::Blob*>(field.get());
            call->result.type = SQLITE_BLOB;
            call->result.str.assign(f->value, f->length);
        } break;
        default:
            call->result.type = SQLITE_NULL;
    }
}

static void ExecuteOnJsThread(napi_env nenv, FuncCall* call) {
    // Aggregate cleanups must not touch the registration or the database:
    // they can outlive both (a finalize path can enqueue one with nothing
    // in flight, after which an exclusive removal may destroy the holder).
    if (call->kind == FuncCall::kAggCleanup) {
        delete call->agg;
        call->agg = NULL;
        return;
    }

    Napi::Env env(nenv);
    Napi::HandleScope scope(env);

    JsFunc* fn = call->fn;

    // Copy every JS-side input out of the holder BEFORE running any user
    // JS: the callback is allowed to re-register or remove this very
    // function, which destroys the holder mid-call. The locals stay valid
    // for the rest of the round trip; `fn` is never read again.
    const bool dead = fn->dead;
    const std::string name = fn->name;
    Napi::Function scalar = fn->fn.IsEmpty() ? Napi::Function() : fn->fn.Value();
    Napi::Function start_f = fn->start.IsEmpty() ? Napi::Function() : fn->start.Value();
    Napi::Function step_f = fn->step.IsEmpty() ? Napi::Function() : fn->step.Value();
    Napi::Function result_f = fn->result.IsEmpty() ? Napi::Function() : fn->result.Value();
    Napi::Function inverse_f = fn->inverse.IsEmpty() ? Napi::Function() : fn->inverse.Value();
    // The integer mode at invocation time: user JS reached from here could
    // configure() a new one, which must not affect this call's arguments.
    const int integer_mode = call->db->integer_mode;
    Database* db = call->db;

    if (dead) {
        call->errored = true;
        call->error = RemovedMidFlightMessage(name);
        return;
    }

    // Function arguments (collations: the two compared values).
    std::vector<Napi::Value> argv;
    argv.reserve(call->args.size());
    for (size_t i = 0; i < call->args.size(); i++) {
        std::string what;
        if (call->kind == FuncCall::kCollation) {
            what = "a value compared by collation '" + name + "'";
        }
        else {
            what = "argument " + std::to_string(i + 1) +
                " of function '" + name + "'";
        }
        argv.push_back(CellToJS(env, call->args[i], integer_mode, what));
        if (env.IsExceptionPending()) {
            SetCallError(db, call,
                "cannot pass " + what + " to JavaScript: ");
            return;
        }
    }

    switch (call->kind) {
    case FuncCall::kScalar: {
        Napi::Value r = scalar.Call(env.Undefined(), argv);
        if (env.IsExceptionPending()) {
            SetCallError(db, call,
                "user-defined function '" + name + "' threw: ");
            return;
        }
        ConvertReturnValue(db, call, r, name);
    } break;

    case FuncCall::kStep:
    case FuncCall::kInverse: {
        const bool inverse = (call->kind == FuncCall::kInverse);
        AggState* agg = (call->agg_slot && *call->agg_slot)
            ? *call->agg_slot : NULL;
        if (agg == NULL) {
            agg = new AggState();
            *call->agg_slot = agg;
        }
        if (!agg->has_acc) {
            if (start_f.IsEmpty()) {
                agg->failed = true;
                call->errored = true;
                call->error = "aggregate function '" + name +
                    "' has no start implementation";
                return;
            }
            Napi::Value acc = start_f.Call(env.Undefined(), {});
            if (env.IsExceptionPending()) {
                agg->failed = true;
                SetCallError(db, call,
                    "aggregate function '" + name + "' threw in start: ");
                return;
            }
            agg->acc = Napi::Persistent(acc);
            agg->has_acc = true;
        }
        std::vector<Napi::Value> cargs;
        cargs.reserve(argv.size() + 1);
        cargs.push_back(agg->acc.Value());
        cargs.insert(cargs.end(), argv.begin(), argv.end());
        Napi::Function& impl = inverse ? inverse_f : step_f;
        Napi::Value next = impl.Call(env.Undefined(), cargs);
        if (env.IsExceptionPending()) {
            agg->failed = true;
            SetCallError(db, call, std::string("aggregate function '") +
                name + "' threw in " + (inverse ? "inverse" : "step") + ": ");
            return;
        }
        // Whatever the implementation returns is the new accumulator,
        // including undefined (a mutating implementation's return value is
        // its own business; the documented contract is return-the-acc).
        agg->acc = Napi::Persistent(next);
    } break;

    case FuncCall::kFinal:
    case FuncCall::kValue: {
        const bool keep_state = (call->kind == FuncCall::kValue);
        AggState* agg = (call->agg_slot && *call->agg_slot)
            ? *call->agg_slot : NULL;
        if (agg != NULL && agg->failed) {
            // A step already failed: the statement carries that error.
            // Unreachable through the worker (it filters failed states
            // before the round trip); pure defense. xFinal frees the
            // state; xValue leaves it for the aborting statement's xFinal.
            if (!keep_state) {
                *call->agg_slot = NULL;
                delete agg;
            }
            call->result.type = SQLITE_NULL;
            return;
        }
        Napi::Value acc;
        if (agg != NULL && agg->has_acc) {
            acc = agg->acc.Value();
        }
        else {
            // Empty group / empty window frame: materialize start().
            if (start_f.IsEmpty()) {
                call->errored = true;
                call->error = "aggregate function '" + name +
                    "' has no start implementation";
                return;
            }
            acc = start_f.Call(env.Undefined(), {});
            if (env.IsExceptionPending()) {
                if (!keep_state && agg != NULL) {
                    *call->agg_slot = NULL;
                    delete agg;
                }
                SetCallError(db, call,
                    "aggregate function '" + name + "' threw in start: ");
                return;
            }
        }
        Napi::Value r = result_f.Call(env.Undefined(), { acc });
        if (!keep_state && agg != NULL) {
            // The aggregate is over: the state must not outlive xFinal.
            *call->agg_slot = NULL;
            delete agg;
        }
        if (env.IsExceptionPending()) {
            SetCallError(db, call,
                "aggregate function '" + name + "' threw in result: ");
            return;
        }
        ConvertReturnValue(db, call, r, name);
    } break;

    case FuncCall::kCollation: {
        Napi::Value r = scalar.Call(env.Undefined(), argv);
        if (env.IsExceptionPending()) {
            SetCallError(db, call,
                "collation '" + name + "' comparator threw: ");
            return;
        }
        if (r.IsNumber()) {
            double d = r.As<Napi::Number>().DoubleValue();
            call->result.type = SQLITE_INTEGER;
            call->result.integer = d < 0 ? -1 : (d > 0 ? 1 : 0);
        }
        else {
            // Collations have no error channel; the worker interrupts the
            // query (see JsCollation). Synthesize the thrown value so the
            // interrupt's failure carries a cause.
            std::string msg = "collation '" + name +
                "' comparator must return a number, got " + JsTypeName(r);
            Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
            SetCallError(db, call, "");
        }
    } break;

    case FuncCall::kProgress: {
        // The JS progress callback (db.progress(n, cb)). Truthy return
        // aborts the running statement; a throw aborts it too, with the
        // thrown error kept as the interrupt failure's cause.
        Napi::Value r = scalar.Call(env.Undefined(), {});
        if (env.IsExceptionPending()) {
            SetCallError(db, call,
                "progress callback of this connection threw: ");
            return;
        }
        call->result.type = SQLITE_INTEGER;
        call->result.integer = r.ToBoolean().Value() ? 1 : 0;
    } break;

    case FuncCall::kAggCleanup:
        break; // handled above
    }
}

// The raw tsfn call-js callback. `data` is a FuncCall*; the napi_env is
// taken from the callback itself, never from the database (cleanup calls
// can outlive their Database).
static void TsfnCallJs(napi_env nenv, napi_value /*jsCallback*/,
        void* /*context*/, void* data) {
    if (data == NULL) return; // loop-teardown notification
    FuncCall* call = static_cast<FuncCall*>(data);
    ExecuteOnJsThread(nenv, call);
    // No exception may escape into the tsfn dispatch machinery. The check
    // must be napi_is_exception_pending: napi_get_and_clear_last_exception
    // alone reports a non-NULL "last exception" even after every exception
    // has been handled.
    bool stray_exception = false;
    napi_is_exception_pending(nenv, &stray_exception);
    if (stray_exception) {
        napi_value pending = NULL;
        napi_get_and_clear_last_exception(nenv, &pending);
        if (!call->errored) {
            call->errored = true;
            call->error = "internal error while invoking a user-defined "
                "function";
        }
    }
    if (call->fire_and_forget) {
        // The worker moved on the moment the enqueue succeeded.
        DisposeFuncCall(call);
        return;
    }
    uv_mutex_lock(&call->mutex);
    call->done = true;
    uv_cond_signal(&call->cond);
    uv_mutex_unlock(&call->mutex);
}
// Creates the per-database channel on first registration. Returns false
// only if napi_create_threadsafe_function failed, which in practice means
// a fatal condition; the caller reports and still releases the database.
static bool EnsureChannel(Database* db) {
    if (db->js_channel != NULL) return true;
    Napi::Env env = db->Env();
    // The `func` is never invoked (TsfnCallJs ignores it); a real function
    // is passed because napi tolerates NULL only when a custom call-js
    // callback is provided, and the no-op keeps both paths valid.
    Napi::Function noop = Napi::Function::New(env,
        [](const Napi::CallbackInfo& info) {
            return info.Env().Undefined();
        });
    napi_value resource_name = Napi::String::New(env,
        "sqlite3.Database.JsFunction");
    napi_threadsafe_function tsfn = NULL;
    napi_status st = napi_create_threadsafe_function(env, noop, NULL,
        resource_name,
        0,  // unbounded queue: blocking calls never block on the queue
        1,  // exactly one Release, from RemoveUserFunctions
        NULL, NULL, NULL, TsfnCallJs, &tsfn);
    if (st != napi_ok || tsfn == NULL) {
        return false;
    }
    // A registered function must not keep the event loop alive by itself
    // (the process-exit test): round trips only happen under in-flight
    // statement work, which holds the loop through its own async work.
    napi_unref_threadsafe_function(env, tsfn);
    db->js_channel = tsfn;
    return true;
}

// Drops holders whose registration sqlite already destroyed (a replace
// fires the old registration's xDestroy). Exclusive context, so nothing
// can be in flight against them.
static void SweepDeadRegistrations(Database* db) {
    auto sweep = [](std::vector<JsFunc*>& v) {
        for (auto it = v.begin(); it != v.end();) {
            if ((*it)->dead) {
                delete *it;
                it = v.erase(it);
            }
            else {
                ++it;
            }
        }
    };
    sweep(db->js_functions);
    sweep(db->js_collations);
}

static void ReleaseChannelIfIdle(Database* db) {
    if (db->js_channel != NULL && db->js_functions.empty()
            && db->js_collations.empty() && db->js_progress == NULL) {
        napi_release_threadsafe_function(db->js_channel,
            napi_tsfn_release);
        db->js_channel = NULL;
    }
}

// Registration failures have no caller callback to reach (the JS layer
// treats registration as synchronous); report on the connection's 'error'
// event, like exec and the other callback-less failures do. Safe to call
// repeatedly: an emit('error') with no listener throws, and building a
// Napi error while that exception is still pending is a fatal napi error,
// so the second failure rides on the first instead of compounding it.
static void ReportRegistrationError(Database* db, int rc) {
    Napi::Env env = db->Env();
    if (env.IsExceptionPending()) return;
    Napi::HandleScope scope(env);
    EXCEPTION(sqlite3_errmsg(db->_handle), rc, exception);
    Napi::Value info[] = {
        Napi::String::New(env, "error"),
        exception
    };
    EMIT_EVENT(db->Value(), 2, info);
}
};

} // namespace node_sqlite3

namespace {

// The re-entrant path for user functions invoked from the synchronous
// methods (Phase 2). sync_sqlite_depth > 0 can only be observed on the JS
// thread inside a *Sync call — this thread is the one executing SQL — so
// the implementation can be invoked directly, exactly like node:sqlite
// does, with no round trip. A throw inside the callback is reported
// through sqlite3_result_error (the error channel functions have); the
// thrown value is kept on the database as the step failure's `cause` by
// SetCallError, as on the async path.
//
// `call` is owned by the caller (deleted after this returns).
void RunDirectOnJsThread(sqlite3_context* ctx, FuncCall* call,
        bool has_result) {
    node_sqlite3::UserFunctionOps::ExecuteOnJsThread(call->db->Env(), call);
    // No exception may escape into sqlite's C frames (and beyond, into the
    // sync caller's napi transition); always clear a stray one — the same
    // rule as TsfnCallJs, and the check must be napi_is_exception_pending.
    bool stray_exception = false;
    napi_is_exception_pending(call->db->Env(), &stray_exception);
    if (stray_exception) {
        napi_value pending = NULL;
        napi_get_and_clear_last_exception(call->db->Env(), &pending);
        if (!call->errored) {
            call->errored = true;
            call->error = "internal error while invoking a user-defined "
                "function";
        }
    }
    if (call->errored) {
        sqlite3_result_error(ctx, call->error.c_str(), -1);
    }
    else if (has_result) {
        ApplyCell(ctx, call->result);
    }
}

} // namespace

// --- sqlite callbacks (worker thread, or the JS thread under the sync guard)

void Database::JsScalarFunc(sqlite3_context* ctx, int argc,
        sqlite3_value** argv) {
    JsFunc* fn = static_cast<JsFunc*>(sqlite3_user_data(ctx));
    Database* db = fn->db;

    if (db->sync_sqlite_depth > 0) {
        // Re-entrant direct call (Phase 2): this is the JS thread inside
        // a *Sync call, so the implementation runs right here.
        FuncCall* call = new FuncCall(fn, FuncCall::kScalar);
        call->args.reserve(argc);
        for (int i = 0; i < argc; i++) {
            call->args.emplace_back();
            ValueToCell(&call->args.back(), argv[i]);
        }
        RunDirectOnJsThread(ctx, call, true);
        delete call;
        return;
    }

    FuncCall* call = new FuncCall(fn, FuncCall::kScalar);
    call->args.reserve(argc);
    for (int i = 0; i < argc; i++) {
        call->args.emplace_back();
        ValueToCell(&call->args.back(), argv[i]);
    }
    UserFunctionOps::RunBlockingRoundTrip(call);
    FinishCall(ctx, call, true);
}

void Database::JsAggregateStep(sqlite3_context* ctx, int argc,
        sqlite3_value** argv) {
    JsFunc* fn = static_cast<JsFunc*>(sqlite3_user_data(ctx));
    Database* db = fn->db;

    if (db->sync_sqlite_depth > 0) {
        FuncCall* call = new FuncCall(fn, FuncCall::kStep);
        call->agg_slot = static_cast<AggState**>(
            sqlite3_aggregate_context(ctx, sizeof(AggState*)));
        if (call->agg_slot == NULL) {
            delete call;
            sqlite3_result_error(ctx, "out of memory", -1);
            return;
        }
        call->args.reserve(argc);
        for (int i = 0; i < argc; i++) {
            call->args.emplace_back();
            ValueToCell(&call->args.back(), argv[i]);
        }
        RunDirectOnJsThread(ctx, call, false);
        delete call;
        return;
    }

    if (fn->dead) {
        sqlite3_result_error(ctx,
            RemovedMidFlightMessage(fn->name).c_str(), -1);
        return;
    }

    // Allocates (zeroed) on the first step of the group; the JS side owns
    // the AggState it stores here.
    AggState** slot = static_cast<AggState**>(
        sqlite3_aggregate_context(ctx, sizeof(AggState*)));
    if (slot == NULL) {
        sqlite3_result_error(ctx, "out of memory", -1);
        return;
    }

    FuncCall* call = new FuncCall(fn, FuncCall::kStep);
    call->agg_slot = slot;
    call->args.reserve(argc);
    for (int i = 0; i < argc; i++) {
        call->args.emplace_back();
        ValueToCell(&call->args.back(), argv[i]);
    }
    UserFunctionOps::RunBlockingRoundTrip(call);
    FinishCall(ctx, call, false);
}

void Database::JsAggregateFinal(sqlite3_context* ctx) {
    JsFunc* fn = static_cast<JsFunc*>(sqlite3_user_data(ctx));
    Database* db = fn->db;

    // sqlite3_aggregate_context(ctx, 0) never allocates: an empty group
    // (or a step that never got past its refusal) has no slot at all.
    AggState** slot = static_cast<AggState**>(
        sqlite3_aggregate_context(ctx, 0));
    AggState* agg = (slot != NULL && *slot != NULL) ? *slot : NULL;

    if (db->sync_sqlite_depth > 0) {
        // Re-entrant direct call (Phase 2): the JS thread can run result()
        // right here and free the accumulator — no deferred cleanup
        // needed, unlike a worker-thread xFinal.
        if (fn->dead) {
            // Removed under a suspended cursor, and this is the JS thread
            // (a *Sync call, Statement::Finalize_, the GC safety net), so
            // the accumulator can be released right here instead of being
            // enqueued. Running result() against a removed registration
            // is not on the table: its implementations may be gone.
            if (agg != NULL) {
                delete agg;
                *slot = NULL;
            }
            sqlite3_result_null(ctx);
            return;
        }
        FuncCall* call = new FuncCall(fn, FuncCall::kFinal);
        call->agg_slot = slot;
        RunDirectOnJsThread(ctx, call, true);
        delete call;
        return;
    }

    if (fn->dead) {
        // Reached after removal, possibly from a main-thread
        // sqlite3_finalize (the GC safety net): free the accumulator
        // without running JS — the cleanup itself is what gets deferred.
        if (agg != NULL) {
            UserFunctionOps::EnqueueAggCleanup(db, fn, agg);
            *slot = NULL;
        }
        sqlite3_result_null(ctx);
        return;
    }

    if (agg != NULL && agg->failed) {
        // A step already failed; the statement carries that error. Just
        // release the accumulator.
        UserFunctionOps::EnqueueAggCleanup(db, fn, agg);
        *slot = NULL;
        sqlite3_result_null(ctx);
        return;
    }

    FuncCall* call = new FuncCall(fn, FuncCall::kFinal);
    call->agg_slot = slot;
    UserFunctionOps::RunBlockingRoundTrip(call);
    FinishCall(ctx, call, true);
}

void Database::JsAggregateValue(sqlite3_context* ctx) {
    JsFunc* fn = static_cast<JsFunc*>(sqlite3_user_data(ctx));
    Database* db = fn->db;

    if (db->sync_sqlite_depth > 0) {
        if (fn->dead) {
            // xValue leaves the state for the aborting statement's
            // xFinal, exactly as the worker path does.
            sqlite3_result_null(ctx);
            return;
        }
        FuncCall* call = new FuncCall(fn, FuncCall::kValue);
        call->agg_slot = static_cast<AggState**>(
            sqlite3_aggregate_context(ctx, 0));
        RunDirectOnJsThread(ctx, call, true);
        delete call;
        return;
    }

    if (fn->dead) {
        sqlite3_result_null(ctx);
        return;
    }

    AggState** slot = static_cast<AggState**>(
        sqlite3_aggregate_context(ctx, 0));
    FuncCall* call = new FuncCall(fn, FuncCall::kValue);
    call->agg_slot = slot;
    UserFunctionOps::RunBlockingRoundTrip(call);
    FinishCall(ctx, call, true);
}

void Database::JsAggregateInverse(sqlite3_context* ctx, int argc,
        sqlite3_value** argv) {
    JsFunc* fn = static_cast<JsFunc*>(sqlite3_user_data(ctx));
    Database* db = fn->db;

    if (db->sync_sqlite_depth > 0) {
        FuncCall* call = new FuncCall(fn, FuncCall::kInverse);
        call->agg_slot = static_cast<AggState**>(
            sqlite3_aggregate_context(ctx, sizeof(AggState*)));
        if (call->agg_slot == NULL) {
            delete call;
            sqlite3_result_error(ctx, "out of memory", -1);
            return;
        }
        call->args.reserve(argc);
        for (int i = 0; i < argc; i++) {
            call->args.emplace_back();
            ValueToCell(&call->args.back(), argv[i]);
        }
        RunDirectOnJsThread(ctx, call, false);
        delete call;
        return;
    }

    if (fn->dead) {
        sqlite3_result_error(ctx,
            RemovedMidFlightMessage(fn->name).c_str(), -1);
        return;
    }

    AggState** slot = static_cast<AggState**>(
        sqlite3_aggregate_context(ctx, sizeof(AggState*)));
    if (slot == NULL) {
        sqlite3_result_error(ctx, "out of memory", -1);
        return;
    }

    FuncCall* call = new FuncCall(fn, FuncCall::kInverse);
    call->agg_slot = slot;
    call->args.reserve(argc);
    for (int i = 0; i < argc; i++) {
        call->args.emplace_back();
        ValueToCell(&call->args.back(), argv[i]);
    }
    UserFunctionOps::RunBlockingRoundTrip(call);
    FinishCall(ctx, call, false);
}

int Database::JsCollation(void* ctx, int len1, const void* d1,
        int len2, const void* d2) {
    JsFunc* fn = static_cast<JsFunc*>(ctx);
    Database* db = fn->db;

    if (db->sync_sqlite_depth > 0 || fn->dead) {
        // Unreachable through the public API (the sync methods refuse to
        // run while a JS collation is registered), but a collation has no
        // error channel: if it ever got here, the only sound answer is to
        // kill the query before a mis-comparison can escape.
        sqlite3_interrupt(db->_handle);
        return 0;
    }

    FuncCall* call = new FuncCall(fn, FuncCall::kCollation);
    call->args.reserve(2);
    call->args.emplace_back(Cell(SQLITE_TEXT));
    if (len1 > 0 && d1 != NULL) {
        call->args.back().str.assign(static_cast<const char*>(d1), len1);
    }
    call->args.emplace_back(Cell(SQLITE_TEXT));
    if (len2 > 0 && d2 != NULL) {
        call->args.back().str.assign(static_cast<const char*>(d2), len2);
    }

    UserFunctionOps::RunBlockingRoundTrip(call);

    int r = 0;
    if (call->errored) {
        // The comparator threw or returned a non-number (the JS side kept
        // the thrown value as the pending cause). Interrupting aborts the
        // whole query — connection-wide, like every cancellation here —
        // rather than letting wrongly-ordered rows escape; returning 0 is
        // fine because this comparison's result is then never used.
        sqlite3_interrupt(db->_handle);
    }
    else if (call->result.type == SQLITE_INTEGER) {
        int64_t v = call->result.integer;
        r = v < 0 ? -1 : (v > 0 ? 1 : 0);
    }
    else if (call->result.type == SQLITE_FLOAT) {
        double v = call->result.real;
        r = v < 0 ? -1 : (v > 0 ? 1 : 0);
    }
    else {
        sqlite3_interrupt(db->_handle);
    }
    DisposeFuncCall(call);
    return r;
}

void Database::JsFuncDestroy(void* data) {
    // Runs on the JS thread by construction: every unregister/replace/close
    // of a registration happens from the exclusive handlers,
    // RemoveUserFunctions (Work_BeginClose, ~Database) or sqlite3_close in
    // the destructor — all main-thread. Marks the holder dead for any
    // round trip queued behind another, releases its JS references and its
    // database ref. The registry owner deletes the struct afterwards.
    JsFunc* fn = static_cast<JsFunc*>(data);
    if (fn->dead) return;
    fn->dead = true;
    fn->fn.Reset();
    fn->start.Reset();
    fn->step.Reset();
    fn->result.Reset();
    fn->inverse.Reset();
    fn->db->Unref();
}

// --- JS-visible entry points -----------------------------------------------

// Registrations (and removals) dispatch inline through Database::Process
// when nothing else is queued — including from inside a user function a
// *Sync call just invoked, mid-step. Redefining a function while sqlite is
// running it (or swapping the implementations a stepping VM may still
// hold) is not a supported sqlite operation, so refuse loudly there; the
// call site can register after the query completes.
static bool RegistrationBlockedBySyncCall(Database* db, Napi::Env env) {
    if (db->sync_sqlite_depth == 0) return false;
    Napi::Error::New(env,
        "user functions cannot be registered or removed from inside a "
        "JavaScript callback invoked by a synchronous method on this "
        "connection: sqlite is mid-step on the implementation being "
        "swapped. Register or remove it before or after the query"
    ).ThrowAsJavaScriptException();
    return true;
}

Napi::Value Database::RegisterUserFunction(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    auto* db = this;

    REQUIRE_ARGUMENT_STRING(0, name);
    REQUIRE_ARGUMENT_INTEGER(1, nArg);
    REQUIRE_ARGUMENT_INTEGER(2, flags);
    REQUIRE_ARGUMENT_FUNCTION(3, fn);
    if (RegistrationBlockedBySyncCall(db, env)) return env.Null();

    auto* baton = new FunctionBaton(db, Napi::Function(), name.c_str(),
        nArg, flags);
    baton->fn.Reset(fn, 1);
    db->Schedule(Work_RegisterFunction, baton, true);

    return info.This();
}

Napi::Value Database::RegisterUserAggregate(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    auto* db = this;

    REQUIRE_ARGUMENT_STRING(0, name);
    REQUIRE_ARGUMENT_INTEGER(1, nArg);
    REQUIRE_ARGUMENT_INTEGER(2, flags);
    // start, step, result required; inverse optional (a non-undefined
    // inverse makes this a window function).
    REQUIRE_ARGUMENT_FUNCTION(3, start);
    REQUIRE_ARGUMENT_FUNCTION(4, step);
    REQUIRE_ARGUMENT_FUNCTION(5, result);
    if (RegistrationBlockedBySyncCall(db, env)) return env.Null();
    Napi::Function inverse;
    if (info.Length() > 6 && !info[6].IsUndefined()) {
        if (!info[6].IsFunction()) {
            Napi::TypeError::New(env,
                "Argument 6 must be a function"
            ).ThrowAsJavaScriptException();
            return env.Null();
        }
        inverse = info[6].As<Napi::Function>();
    }

    auto* baton = new FunctionBaton(db, Napi::Function(), name.c_str(),
        nArg, flags);
    baton->start.Reset(start, 1);
    baton->step.Reset(step, 1);
    baton->result.Reset(result, 1);
    if (!inverse.IsEmpty()) baton->inverse.Reset(inverse, 1);
    db->Schedule(Work_RegisterAggregate, baton, true);

    return info.This();
}

Napi::Value Database::RegisterUserCollation(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    auto* db = this;

    REQUIRE_ARGUMENT_STRING(0, name);
    REQUIRE_ARGUMENT_FUNCTION(1, fn);
    if (RegistrationBlockedBySyncCall(db, env)) return env.Null();

    auto* baton = new FunctionBaton(db, Napi::Function(), name.c_str(),
        0, 0);
    baton->is_collation = true;
    baton->fn.Reset(fn, 1);
    db->Schedule(Work_RegisterCollation, baton, true);

    return info.This();
}

Napi::Value Database::RemoveUserFunction(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    auto* db = this;

    REQUIRE_ARGUMENT_STRING(0, name);
    if (RegistrationBlockedBySyncCall(db, env)) return env.Null();

    auto* baton = new RemoveFunctionBaton(db, Napi::Function(), name.c_str());
    baton->collation = false;
    db->Schedule(Work_RemoveFunction, baton, true);

    return info.This();
}

Napi::Value Database::RemoveUserCollation(const Napi::CallbackInfo& info) {
    auto env = info.Env();
    auto* db = this;

    REQUIRE_ARGUMENT_STRING(0, name);
    if (RegistrationBlockedBySyncCall(db, env)) return env.Null();

    auto* baton = new RemoveFunctionBaton(db, Napi::Function(), name.c_str());
    baton->collation = true;
    db->Schedule(Work_RemoveCollation, baton, true);

    return info.This();
}

// --- Registration handlers (main thread, exclusive) -------------------------

void Database::Work_RegisterFunction(Baton* b) {
    auto baton = std::unique_ptr<FunctionBaton>(static_cast<FunctionBaton*>(b));
    auto* db = baton->db;

    assert(db->IsOpen());
    assert(db->_handle);
    assert(db->pending == 0);

    UserFunctionOps::SweepDeadRegistrations(db);
    if (!UserFunctionOps::EnsureChannel(db)) {
        UserFunctionOps::ReportRegistrationError(db, SQLITE_NOMEM);
        db->exclusiveHeld = false;
        db->Process();
        return;
    }

    auto* holder = new JsFunc(db, baton->name, baton->nArg);
    holder->fn = std::move(baton->fn);

    // xDestroy is invoked with the pApp pointer, which is also the user
    // data the callbacks receive.
    int rc = sqlite3_create_function_v2(db->_handle,
        baton->name.c_str(), baton->nArg, SQLITE_UTF8 | baton->flags,
        holder, JsScalarFunc, NULL, NULL, JsFuncDestroy);
    if (rc != SQLITE_OK) {
        UserFunctionOps::ReportRegistrationError(db, rc);
        JsFuncDestroy(holder);
        delete holder;
    }
    else {
        db->js_functions.push_back(holder);
    }

    // Inline like Work_Wait: the exclusive call completes here, so the
    // database is released for whatever Process dispatches next.
    db->exclusiveHeld = false;
    db->Process();
}

void Database::Work_RegisterAggregate(Baton* b) {
    auto baton = std::unique_ptr<FunctionBaton>(static_cast<FunctionBaton*>(b));
    auto* db = baton->db;

    assert(db->IsOpen());
    assert(db->_handle);
    assert(db->pending == 0);

    UserFunctionOps::SweepDeadRegistrations(db);
    if (!UserFunctionOps::EnsureChannel(db)) {
        UserFunctionOps::ReportRegistrationError(db, SQLITE_NOMEM);
        db->exclusiveHeld = false;
        db->Process();
        return;
    }

    const bool is_window = !baton->inverse.IsEmpty();
    auto* holder = new JsFunc(db, baton->name, baton->nArg);
    holder->start = std::move(baton->start);
    holder->step = std::move(baton->step);
    holder->result = std::move(baton->result);
    if (is_window) holder->inverse = std::move(baton->inverse);

    int rc;
    if (is_window) {
        // sqlite3_create_window_function has no flags slot (its eTextRep
        // must be SQLITE_UTF8): deterministic/directOnly/innocuous cannot
        // be applied to window functions. Documented in the JS layer.
        rc = sqlite3_create_window_function(db->_handle,
            baton->name.c_str(), baton->nArg, SQLITE_UTF8, holder,
            JsAggregateStep, JsAggregateFinal, JsAggregateValue,
            JsAggregateInverse, JsFuncDestroy);
    }
    else {
        rc = sqlite3_create_function_v2(db->_handle,
            baton->name.c_str(), baton->nArg, SQLITE_UTF8 | baton->flags,
            holder, NULL, JsAggregateStep, JsAggregateFinal,
            JsFuncDestroy);
    }
    if (rc != SQLITE_OK) {
        UserFunctionOps::ReportRegistrationError(db, rc);
        JsFuncDestroy(holder);
        delete holder;
    }
    else {
        db->js_functions.push_back(holder);
    }

    db->exclusiveHeld = false;
    db->Process();
}

void Database::Work_RegisterCollation(Baton* b) {
    auto baton = std::unique_ptr<FunctionBaton>(static_cast<FunctionBaton*>(b));
    auto* db = baton->db;

    assert(db->IsOpen());
    assert(db->_handle);
    assert(db->pending == 0);

    UserFunctionOps::SweepDeadRegistrations(db);
    if (!UserFunctionOps::EnsureChannel(db)) {
        UserFunctionOps::ReportRegistrationError(db, SQLITE_NOMEM);
        db->exclusiveHeld = false;
        db->Process();
        return;
    }

    auto* holder = new JsFunc(db, baton->name, 0);
    holder->is_collation = true;
    holder->fn = std::move(baton->fn);

    int rc = sqlite3_create_collation(db->_handle, baton->name.c_str(),
        SQLITE_UTF8, holder, JsCollation);
    if (rc != SQLITE_OK) {
        UserFunctionOps::ReportRegistrationError(db, rc);
        JsFuncDestroy(holder);
        delete holder;
    }
    else {
        db->js_collations.push_back(holder);
    }

    db->exclusiveHeld = false;
    db->Process();
}

void Database::Work_RemoveFunction(Baton* b) {
    auto baton = std::unique_ptr<RemoveFunctionBaton>(
        static_cast<RemoveFunctionBaton*>(b));
    auto* db = baton->db;

    assert(db->IsOpen());
    assert(db->_handle);
    assert(db->pending == 0);

    bool failed = false;
    for (auto it = db->js_functions.begin(); it != db->js_functions.end();) {
        JsFunc* fn = *it;
        if (fn->dead) {
            // A replaced registration sqlite already destroyed: just drop
            // the holder instead of unregistering through it (which would
            // hit the live replacement and double-report failures).
            delete fn;
            it = db->js_functions.erase(it);
            continue;
        }
        if (fn->name == baton->name) {
            // All-NULL callbacks delete the registration; the old
            // registration's xDestroy fires from inside this call.
            int rc = sqlite3_create_function_v2(db->_handle,
                fn->name.c_str(), fn->nArg, SQLITE_UTF8,
                NULL, NULL, NULL, NULL, NULL);
            if (rc == SQLITE_OK) {
                JsFuncDestroy(fn);
                delete fn;
                it = db->js_functions.erase(it);
                continue;
            }
            // SQLITE_BUSY: a suspended cursor counts as an active VM, so
            // sqlite refuses. Keep the registration and say so.
            failed = true;
            UserFunctionOps::ReportRegistrationError(db, rc);
        }
        ++it;
    }

    if (!failed) UserFunctionOps::SweepDeadRegistrations(db);
    UserFunctionOps::ReleaseChannelIfIdle(db);

    db->exclusiveHeld = false;
    db->Process();
}

void Database::Work_RemoveCollation(Baton* b) {
    auto baton = std::unique_ptr<RemoveFunctionBaton>(
        static_cast<RemoveFunctionBaton*>(b));
    auto* db = baton->db;

    assert(db->IsOpen());
    assert(db->_handle);
    assert(db->pending == 0);

    for (auto it = db->js_collations.begin(); it != db->js_collations.end();) {
        JsFunc* fn = *it;
        if (fn->dead) {
            delete fn;
            it = db->js_collations.erase(it);
            continue;
        }
        if (fn->name == baton->name) {
            int rc = sqlite3_create_collation(db->_handle,
                fn->name.c_str(), SQLITE_UTF8, NULL, NULL);
            if (rc == SQLITE_OK) {
                JsFuncDestroy(fn);
                delete fn;
                it = db->js_collations.erase(it);
                continue;
            }
            UserFunctionOps::ReportRegistrationError(db, rc);
        }
        ++it;
    }

    UserFunctionOps::ReleaseChannelIfIdle(db);

    db->exclusiveHeld = false;
    db->Process();
}

void Database::RemoveUserFunctions() {
    // Called from Work_BeginClose and ~Database: main thread, nothing in
    // flight. Registrations sqlite refuses to drop (a suspended cursor
    // makes the close fail too) stay alive with their holders so the
    // still-usable connection keeps working.
    if (_handle != NULL) {
        for (auto it = js_functions.begin(); it != js_functions.end();) {
            JsFunc* fn = *it;
            if (fn->dead) {
                delete fn;
                it = js_functions.erase(it);
                continue;
            }
            int rc = sqlite3_create_function_v2(_handle,
                fn->name.c_str(), fn->nArg, SQLITE_UTF8,
                NULL, NULL, NULL, NULL, NULL);
            if (rc == SQLITE_OK) {
                JsFuncDestroy(fn);
                delete fn;
                it = js_functions.erase(it);
            }
            else {
                ++it;
            }
        }
        for (auto it = js_collations.begin(); it != js_collations.end();) {
            JsFunc* fn = *it;
            if (fn->dead) {
                delete fn;
                it = js_collations.erase(it);
                continue;
            }
            int rc = sqlite3_create_collation(_handle, fn->name.c_str(),
                SQLITE_UTF8, NULL, NULL);
            if (rc == SQLITE_OK) {
                JsFuncDestroy(fn);
                delete fn;
                it = js_collations.erase(it);
            }
            else {
                ++it;
            }
        }
    }
    else {
        // The handle is gone (never opened, or a prior close completed in
        // a prior destructor call): no sqlite state to unregister.
        for (JsFunc* fn : js_functions) {
            JsFuncDestroy(fn);
            delete fn;
        }
        js_functions.clear();
        for (JsFunc* fn : js_collations) {
            JsFuncDestroy(fn);
            delete fn;
        }
        js_collations.clear();
    }
    // Deliberately NOT releasing the channel here: RemoveUserFunctions
    // also runs from ~Database, which the environment can invoke from
    // inside the channel's own teardown — releasing it there re-enters
    // the tsfn destructor and hangs the process at exit. The channel is
    // unref'd, so an unreleased one costs nothing while the loop lives,
    // and environment teardown destroys it regardless.
}

void Database::ReleaseJsChannelIfIdle() {
    UserFunctionOps::ReleaseChannelIfIdle(this);
}

bool Database::EnsureJsChannel() {
    return UserFunctionOps::EnsureChannel(this);
}

void Database::ReportRegistrationFailure(int rc) {
    UserFunctionOps::ReportRegistrationError(this, rc);
}

// The sqlite progress handler (Deliverable 07). Flag mode is one relaxed
// atomic load — set from any thread via the SharedArrayBuffer-backed
// cancellation token. Callback mode makes the standard blocking round
// trip; the sync methods are gated against it (BindSync and the sync
// prepare path), and the depth check below is belt-and-braces for any
// path that slips through: continuing is the only non-deadlocking
// answer there, since the handler cannot report an error.
int Database::ProgressHandler(void* ctx) {
    auto* db = static_cast<Database*>(ctx);
    switch (db->progress_mode) {
        case ProgressMode::Flag:
            return db->progress_flag->load(std::memory_order_relaxed) != 0
                ? 1 : 0;
        case ProgressMode::Callback: {
            JsFunc* fn = db->js_progress;
            if (fn == NULL || fn->dead) return 0;
            if (db->sync_sqlite_depth > 0) return 0;
            FuncCall* call = new FuncCall(fn, FuncCall::kProgress);
            UserFunctionOps::RunBlockingRoundTrip(call);
            bool stop = !call->errored && call->result.type == SQLITE_INTEGER
                && call->result.integer != 0;
            bool failed = call->errored;
            DisposeFuncCall(call);
            if (failed) {
                // The callback threw: the thrown value is already stashed
                // as the pending cause; abort the statement so the
                // SQLITE_INTERRUPT failure carries it.
                sqlite3_interrupt(db->_handle);
                return 1;
            }
            return stop ? 1 : 0;
        }
        default:
            return 0;
    }
}

void Database::SetPendingJsError(Napi::Value err) {
    pending_js_error = Napi::Persistent(err);
}

void Database::AttachPendingJsError(Napi::Object err) {
    if (pending_js_error.IsEmpty()) return;
    err.Set("cause", pending_js_error.Value());
    pending_js_error.Reset();
}
