//! zsqlite - SQLite wrapper for Zig
//!
//! A lightweight, idiomatic Zig wrapper around SQLite that compiles the
//! SQLite amalgamation directly into your binary for zero runtime dependencies.
//!
//! ## Quick Start
//!
//! ```zig
//! const sqlite = @import("zsqlite");
//!
//! pub fn main() !void {
//!     var db = try sqlite.open(":memory:");
//!     defer db.close();
//!
//!     try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
//!
//!     var stmt = try db.prepare("INSERT INTO users (name) VALUES (?)");
//!     defer stmt.finalize();
//!
//!     try stmt.bindText(1, "Alice");
//!     _ = try stmt.step();
//! }
//! ```

const std = @import("std");
const c = @cImport({
    @cInclude("sqlite3.h");
});

/// SQLite error codes mapped to Zig errors
pub const Error = error{
    /// Generic error
    SqliteError,
    /// Internal logic error in SQLite
    InternalError,
    /// Access permission denied
    PermissionDenied,
    /// Callback routine requested an abort
    Abort,
    /// The database file is locked
    Busy,
    /// A table in the database is locked
    Locked,
    /// A malloc() failed
    OutOfMemory,
    /// Attempt to write a readonly database
    ReadOnly,
    /// Operation terminated by sqlite3_interrupt()
    Interrupt,
    /// Some kind of disk I/O error occurred
    IoError,
    /// The database disk image is malformed
    Corrupt,
    /// Unknown opcode in sqlite3_file_control()
    NotFound,
    /// Insertion failed because database is full
    Full,
    /// Unable to open the database file
    CantOpen,
    /// Database lock protocol error
    Protocol,
    /// Internal use only
    Empty,
    /// The database schema changed
    Schema,
    /// String or BLOB exceeds size limit
    TooBig,
    /// Abort due to constraint violation
    Constraint,
    /// Data type mismatch
    Mismatch,
    /// Library used incorrectly
    Misuse,
    /// Uses OS features not supported on host
    NoLfs,
    /// Authorization denied
    Auth,
    /// Not used
    Format,
    /// Parameter out of range
    Range,
    /// File opened that is not a database file
    NotADatabase,
    /// Notifications from sqlite3_log()
    Notice,
    /// Warnings from sqlite3_log()
    Warning,
    /// sqlite3_step() has another row ready
    Row,
    /// sqlite3_step() has finished executing
    Done,
};

/// Converts a SQLite result code to a Zig error
fn resultToError(result: c_int) Error {
    return switch (result) {
        c.SQLITE_ERROR => Error.SqliteError,
        c.SQLITE_INTERNAL => Error.InternalError,
        c.SQLITE_PERM => Error.PermissionDenied,
        c.SQLITE_ABORT => Error.Abort,
        c.SQLITE_BUSY => Error.Busy,
        c.SQLITE_LOCKED => Error.Locked,
        c.SQLITE_NOMEM => Error.OutOfMemory,
        c.SQLITE_READONLY => Error.ReadOnly,
        c.SQLITE_INTERRUPT => Error.Interrupt,
        c.SQLITE_IOERR => Error.IoError,
        c.SQLITE_CORRUPT => Error.Corrupt,
        c.SQLITE_NOTFOUND => Error.NotFound,
        c.SQLITE_FULL => Error.Full,
        c.SQLITE_CANTOPEN => Error.CantOpen,
        c.SQLITE_PROTOCOL => Error.Protocol,
        c.SQLITE_EMPTY => Error.Empty,
        c.SQLITE_SCHEMA => Error.Schema,
        c.SQLITE_TOOBIG => Error.TooBig,
        c.SQLITE_CONSTRAINT => Error.Constraint,
        c.SQLITE_MISMATCH => Error.Mismatch,
        c.SQLITE_MISUSE => Error.Misuse,
        c.SQLITE_NOLFS => Error.NoLfs,
        c.SQLITE_AUTH => Error.Auth,
        c.SQLITE_FORMAT => Error.Format,
        c.SQLITE_RANGE => Error.Range,
        c.SQLITE_NOTADB => Error.NotADatabase,
        c.SQLITE_NOTICE => Error.Notice,
        c.SQLITE_WARNING => Error.Warning,
        c.SQLITE_ROW => Error.Row,
        c.SQLITE_DONE => Error.Done,
        else => Error.SqliteError,
    };
}

/// SQLite database connection
pub const Database = struct {
    handle: ?*c.sqlite3,

    const Self = @This();

    /// Opens a database connection.
    ///
    /// Use ":memory:" for an in-memory database, or a file path for persistent storage.
    pub fn open(path: [:0]const u8) Error!Self {
        var handle: ?*c.sqlite3 = null;
        const result = c.sqlite3_open(path.ptr, &handle);

        if (result != c.SQLITE_OK) {
            if (handle) |h| {
                _ = c.sqlite3_close(h);
            }
            return resultToError(result);
        }

        return .{ .handle = handle };
    }

    /// Opens a database with specific flags.
    pub fn openWithFlags(path: [:0]const u8, flags: OpenFlags) Error!Self {
        var handle: ?*c.sqlite3 = null;
        const result = c.sqlite3_open_v2(path.ptr, &handle, flags.toInt(), null);

        if (result != c.SQLITE_OK) {
            if (handle) |h| {
                _ = c.sqlite3_close(h);
            }
            return resultToError(result);
        }

        return .{ .handle = handle };
    }

    /// Closes the database connection.
    pub fn close(self: *Self) void {
        if (self.handle) |h| {
            _ = c.sqlite3_close(h);
            self.handle = null;
        }
    }

    /// Executes SQL statement(s) without returning results.
    ///
    /// Suitable for CREATE, INSERT, UPDATE, DELETE, etc.
    pub fn exec(self: *Self, sql: [:0]const u8) Error!void {
        const result = c.sqlite3_exec(self.handle, sql.ptr, null, null, null);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Executes SQL with runtime-known string (copies to add null terminator).
    pub fn execAlloc(self: *Self, allocator: std.mem.Allocator, sql: []const u8) !void {
        const sql_z = try allocator.dupeZ(u8, sql);
        defer allocator.free(sql_z);
        try self.exec(sql_z);
    }

    /// Prepares a SQL statement for execution.
    pub fn prepare(self: *Self, sql: [:0]const u8) Error!Statement {
        var stmt: ?*c.sqlite3_stmt = null;
        const result = c.sqlite3_prepare_v2(self.handle, sql.ptr, @intCast(sql.len + 1), &stmt, null);

        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }

        return .{ .handle = stmt, .db = self };
    }

    /// Prepares a SQL statement with runtime-known string.
    pub fn prepareAlloc(self: *Self, allocator: std.mem.Allocator, sql: []const u8) !Statement {
        const sql_z = try allocator.dupeZ(u8, sql);
        defer allocator.free(sql_z);
        return self.prepare(sql_z);
    }

    /// Returns the rowid of the most recent successful INSERT.
    pub fn lastInsertRowId(self: *Self) i64 {
        return c.sqlite3_last_insert_rowid(self.handle);
    }

    /// Returns the number of rows modified by the most recent statement.
    pub fn changes(self: *Self) i32 {
        return c.sqlite3_changes(self.handle);
    }

    /// Returns the total number of rows modified since connection opened.
    pub fn totalChanges(self: *Self) i32 {
        return c.sqlite3_total_changes(self.handle);
    }

    /// Returns the most recent error message.
    pub fn errorMessage(self: *Self) ?[]const u8 {
        const msg = c.sqlite3_errmsg(self.handle);
        if (msg) |m| {
            return std.mem.span(m);
        }
        return null;
    }

    /// Enables or disables foreign key constraints.
    pub fn setForeignKeys(self: *Self, enabled: bool) Error!void {
        const sql: [:0]const u8 = if (enabled) "PRAGMA foreign_keys = ON" else "PRAGMA foreign_keys = OFF";
        try self.exec(sql);
    }

    /// Begins a transaction.
    pub fn begin(self: *Self) Error!void {
        try self.exec("BEGIN");
    }

    /// Begins an immediate transaction (acquires write lock immediately).
    pub fn beginImmediate(self: *Self) Error!void {
        try self.exec("BEGIN IMMEDIATE");
    }

    /// Commits the current transaction.
    pub fn commit(self: *Self) Error!void {
        try self.exec("COMMIT");
    }

    /// Rolls back the current transaction.
    pub fn rollback(self: *Self) Error!void {
        try self.exec("ROLLBACK");
    }

    /// Begins an exclusive transaction (acquires exclusive lock immediately).
    pub fn beginExclusive(self: *Self) Error!void {
        try self.exec("BEGIN EXCLUSIVE");
    }

    // ========================================================================
    // Savepoints
    // ========================================================================

    /// Creates a savepoint with the given name.
    ///
    /// Savepoints allow nested transactions. Use `release()` to commit
    /// or `rollbackTo()` to revert to the savepoint.
    pub fn savepoint(self: *Self, allocator: std.mem.Allocator, name: []const u8) !void {
        const sql = try std.fmt.allocPrint(allocator, "SAVEPOINT {s}\x00", .{name});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Releases (commits) a savepoint.
    ///
    /// All changes since the savepoint was created become permanent
    /// (within the containing transaction).
    pub fn release(self: *Self, allocator: std.mem.Allocator, name: []const u8) !void {
        const sql = try std.fmt.allocPrint(allocator, "RELEASE SAVEPOINT {s}\x00", .{name});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Rolls back to a savepoint.
    ///
    /// All changes since the savepoint was created are undone.
    /// The savepoint remains active and can be rolled back to again.
    pub fn rollbackTo(self: *Self, allocator: std.mem.Allocator, name: []const u8) !void {
        const sql = try std.fmt.allocPrint(allocator, "ROLLBACK TO SAVEPOINT {s}\x00", .{name});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    // ========================================================================
    // Pragmas and Configuration
    // ========================================================================

    /// Sets the busy timeout in milliseconds.
    ///
    /// When the database is locked, SQLite will wait up to this many
    /// milliseconds before returning SQLITE_BUSY.
    pub fn setBusyTimeout(self: *Self, ms: i32) Error!void {
        const result = c.sqlite3_busy_timeout(self.handle, ms);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Sets the journal mode.
    ///
    /// Common modes: "DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"
    /// WAL mode is recommended for concurrent access.
    pub fn setJournalMode(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
        const sql = try std.fmt.allocPrint(allocator, "PRAGMA journal_mode = {s}\x00", .{mode});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Sets the synchronous mode.
    ///
    /// Modes: "OFF" (0), "NORMAL" (1), "FULL" (2), "EXTRA" (3)
    /// Lower values = faster but less safe, higher = slower but safer.
    pub fn setSynchronous(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
        const sql = try std.fmt.allocPrint(allocator, "PRAGMA synchronous = {s}\x00", .{mode});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Enables or disables WAL mode with recommended settings.
    ///
    /// WAL mode provides better concurrency and is recommended for most uses.
    pub fn enableWalMode(self: *Self, allocator: std.mem.Allocator) !void {
        try self.setJournalMode(allocator, "WAL");
        try self.setSynchronous(allocator, "NORMAL");
    }

    /// Interrupts a long-running query.
    ///
    /// Causes any pending database operation to abort and return SQLITE_INTERRUPT.
    pub fn interrupt(self: *Self) void {
        c.sqlite3_interrupt(self.handle);
    }

    /// Returns the error code of the most recent error.
    pub fn errorCode(self: *Self) i32 {
        return c.sqlite3_errcode(self.handle);
    }

    /// Returns the extended error code of the most recent error.
    pub fn extendedErrorCode(self: *Self) i32 {
        return c.sqlite3_extended_errcode(self.handle);
    }

    /// Returns whether the database is read-only.
    ///
    /// The `db_name` parameter is usually "main" for the main database.
    pub fn isReadOnly(self: *Self, db_name: [:0]const u8) bool {
        return c.sqlite3_db_readonly(self.handle, db_name.ptr) == 1;
    }

    /// Returns the filename of a database.
    ///
    /// The `db_name` parameter is usually "main" for the main database.
    pub fn filename(self: *Self, db_name: [:0]const u8) ?[]const u8 {
        const fname = c.sqlite3_db_filename(self.handle, db_name.ptr);
        if (fname) |f| {
            return std.mem.span(f);
        }
        return null;
    }

    // ========================================================================
    // ATTACH/DETACH databases
    // ========================================================================

    /// Attaches another database file to this connection.
    ///
    /// After attaching, tables from the attached database can be accessed
    /// using the schema name prefix: `SELECT * FROM schema_name.table_name`
    ///
    /// The attached database will be opened read-write if possible.
    pub fn attach(self: *Self, allocator: std.mem.Allocator, file_path: []const u8, schema_name: []const u8) !void {
        const sql = try std.fmt.allocPrint(allocator, "ATTACH DATABASE '{s}' AS {s}\x00", .{ file_path, schema_name });
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Attaches an in-memory database to this connection.
    ///
    /// Creates a new empty in-memory database accessible via the schema name.
    pub fn attachMemory(self: *Self, allocator: std.mem.Allocator, schema_name: []const u8) !void {
        const sql = try std.fmt.allocPrint(allocator, "ATTACH DATABASE ':memory:' AS {s}\x00", .{schema_name});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Detaches a previously attached database.
    ///
    /// All tables from the detached database become inaccessible.
    pub fn detach(self: *Self, allocator: std.mem.Allocator, schema_name: []const u8) !void {
        const sql = try std.fmt.allocPrint(allocator, "DETACH DATABASE {s}\x00", .{schema_name});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Returns a list of attached database names.
    ///
    /// Always includes "main" and "temp".
    pub fn listDatabases(self: *Self, allocator: std.mem.Allocator) ![][]const u8 {
        var stmt = try self.prepare("PRAGMA database_list");
        defer stmt.finalize();

        var list: std.ArrayListUnmanaged([]const u8) = .empty;
        errdefer {
            for (list.items) |item| allocator.free(item);
            list.deinit(allocator);
        }

        while (try stmt.step()) {
            if (stmt.columnText(1)) |name| {
                const owned = try allocator.dupe(u8, name);
                try list.append(allocator, owned);
            }
        }

        return list.toOwnedSlice(allocator);
    }

    /// Frees the list returned by listDatabases.
    pub fn freeDatabaseList(allocator: std.mem.Allocator, list: [][]const u8) void {
        for (list) |item| allocator.free(item);
        allocator.free(list);
    }

    // ========================================================================
    // User-Defined Functions
    // ========================================================================

    /// Registers a scalar function with the database.
    ///
    /// Scalar functions are called once per row and return a single value.
    /// The function wrapper is automatically freed when the database is closed
    /// or when the function is removed.
    ///
    /// Example:
    /// ```zig
    /// fn myDouble(ctx: FunctionContext, args: []const FunctionValue) void {
    ///     if (args.len != 1) {
    ///         ctx.setError("double() requires 1 argument");
    ///         return;
    ///     }
    ///     if (args[0].isNull()) {
    ///         ctx.setNull();
    ///         return;
    ///     }
    ///     ctx.setInt(args[0].asInt() * 2);
    /// }
    ///
    /// try db.createScalarFunction("double", 1, myDouble);
    /// ```
    pub fn createScalarFunction(
        self: *Self,
        name: [:0]const u8,
        num_args: i32,
        func: ScalarFn,
    ) !void {
        // Allocate wrapper to store function pointer (uses page allocator internally)
        const wrapper = try ScalarFnWrapper.create(func);

        const result = c.sqlite3_create_function_v2(
            self.handle,
            name.ptr,
            num_args,
            c.SQLITE_UTF8,
            wrapper,
            scalarCallback,
            null, // step (for aggregates)
            null, // final (for aggregates)
            scalarDestructor,
        );

        if (result != c.SQLITE_OK) {
            wrapper.destroy();
            return resultToError(result);
        }
    }

    /// Removes a previously registered function.
    pub fn removeFunction(self: *Self, name: [:0]const u8, num_args: i32) Error!void {
        const result = c.sqlite3_create_function_v2(
            self.handle,
            name.ptr,
            num_args,
            c.SQLITE_UTF8,
            null,
            null,
            null,
            null,
            null,
        );

        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Registers a custom aggregate function.
    ///
    /// Aggregate functions process multiple rows and return a single result.
    /// They require two callbacks:
    /// - step: Called for each row, accumulates the result
    /// - final: Called once at the end to produce the final result
    ///
    /// Example (sum of squares):
    /// ```zig
    /// const SumSquaresState = struct {
    ///     total: i64 = 0,
    /// };
    ///
    /// fn sumSquaresStep(ctx: AggregateContext, args: []const FunctionValue) void {
    ///     const state = ctx.getAggregateContext(SumSquaresState) orelse return;
    ///     if (args.len > 0 and !args[0].isNull()) {
    ///         const val = args[0].asInt();
    ///         state.total += val * val;
    ///     }
    /// }
    ///
    /// fn sumSquaresFinal(ctx: AggregateContext) void {
    ///     const state = ctx.getAggregateContext(SumSquaresState) orelse {
    ///         ctx.setNull();
    ///         return;
    ///     };
    ///     ctx.setInt(state.total);
    /// }
    ///
    /// try db.createAggregateFunction("sum_squares", 1, sumSquaresStep, sumSquaresFinal);
    /// // SELECT sum_squares(value) FROM numbers;
    /// ```
    pub fn createAggregateFunction(
        self: *Self,
        name: [:0]const u8,
        num_args: i32,
        step_fn: AggregateStepFn,
        final_fn: AggregateFinalFn,
    ) !void {
        const wrapper = try AggregateFnWrapper.create(step_fn, final_fn);

        const result = c.sqlite3_create_function_v2(
            self.handle,
            name.ptr,
            num_args,
            c.SQLITE_UTF8,
            wrapper,
            null, // xFunc (for scalar)
            aggregateStepCallback,
            aggregateFinalCallback,
            aggregateDestructor,
        );

        if (result != c.SQLITE_OK) {
            wrapper.destroy();
            return resultToError(result);
        }
    }

    // ========================================================================
    // Custom Collations
    // ========================================================================

    /// Registers a custom collation sequence.
    ///
    /// Collations define how strings are compared for ORDER BY, comparison
    /// operators, and DISTINCT. The collation is automatically freed when
    /// the database is closed.
    ///
    /// Example:
    /// ```zig
    /// // Case-insensitive collation
    /// fn caseInsensitive(a: []const u8, b: []const u8) i32 {
    ///     // Compare ignoring case...
    /// }
    ///
    /// try db.createCollation("NOCASE2", caseInsensitive);
    ///
    /// // Use in queries:
    /// // SELECT * FROM users ORDER BY name COLLATE NOCASE2
    /// ```
    pub fn createCollation(self: *Self, name: [:0]const u8, func: CollationFn) !void {
        const wrapper = try CollationWrapper.create(func);

        const result = c.sqlite3_create_collation_v2(
            self.handle,
            name.ptr,
            c.SQLITE_UTF8,
            wrapper,
            collationCallback,
            collationDestructor,
        );

        if (result != c.SQLITE_OK) {
            wrapper.destroy();
            return resultToError(result);
        }
    }

    /// Removes a previously registered collation.
    pub fn removeCollation(self: *Self, name: [:0]const u8) Error!void {
        const result = c.sqlite3_create_collation_v2(
            self.handle,
            name.ptr,
            c.SQLITE_UTF8,
            null,
            null,
            null,
        );

        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    // ========================================================================
    // Hooks
    // ========================================================================

    /// Sets a commit hook callback.
    ///
    /// The callback is invoked whenever a transaction is committed.
    /// Return true to allow the commit, false to force a rollback.
    ///
    /// Pass null to remove an existing hook.
    ///
    /// Note: Only one commit hook can be active at a time.
    pub fn setCommitHook(self: *Self, func: ?ZigCommitHookFn) !void {
        if (func) |f| {
            const wrapper = try CommitHookWrapper.create(f);
            const old = c.sqlite3_commit_hook(self.handle, commitHookCallback, wrapper);
            // Destroy old wrapper if it existed
            if (old != null) {
                const old_wrapper: *CommitHookWrapper = @ptrCast(@alignCast(old));
                old_wrapper.destroy();
            }
        } else {
            const old = c.sqlite3_commit_hook(self.handle, null, null);
            if (old != null) {
                const old_wrapper: *CommitHookWrapper = @ptrCast(@alignCast(old));
                old_wrapper.destroy();
            }
        }
    }

    /// Sets a rollback hook callback.
    ///
    /// The callback is invoked whenever a transaction is rolled back.
    ///
    /// Pass null to remove an existing hook.
    ///
    /// Note: Only one rollback hook can be active at a time.
    pub fn setRollbackHook(self: *Self, func: ?ZigRollbackHookFn) !void {
        if (func) |f| {
            const wrapper = try RollbackHookWrapper.create(f);
            const old = c.sqlite3_rollback_hook(self.handle, rollbackHookCallback, wrapper);
            if (old != null) {
                const old_wrapper: *RollbackHookWrapper = @ptrCast(@alignCast(old));
                old_wrapper.destroy();
            }
        } else {
            const old = c.sqlite3_rollback_hook(self.handle, null, null);
            if (old != null) {
                const old_wrapper: *RollbackHookWrapper = @ptrCast(@alignCast(old));
                old_wrapper.destroy();
            }
        }
    }

    /// Sets an update hook callback.
    ///
    /// The callback is invoked whenever a row is inserted, updated, or deleted.
    /// The callback receives:
    /// - operation: .insert, .update, or .delete
    /// - db_name: Database name (e.g., "main")
    /// - table_name: Table name
    /// - rowid: Row ID of the affected row
    ///
    /// Pass null to remove an existing hook.
    ///
    /// Note: Only one update hook can be active at a time.
    pub fn setUpdateHook(self: *Self, func: ?ZigUpdateHookFn) !void {
        if (func) |f| {
            const wrapper = try UpdateHookWrapper.create(f);
            const old = c.sqlite3_update_hook(self.handle, updateHookCallback, wrapper);
            if (old != null) {
                const old_wrapper: *UpdateHookWrapper = @ptrCast(@alignCast(old));
                old_wrapper.destroy();
            }
        } else {
            const old = c.sqlite3_update_hook(self.handle, null, null);
            if (old != null) {
                const old_wrapper: *UpdateHookWrapper = @ptrCast(@alignCast(old));
                old_wrapper.destroy();
            }
        }
    }

    /// Removes all hooks (commit, rollback, update).
    pub fn clearHooks(self: *Self) void {
        const commit_old = c.sqlite3_commit_hook(self.handle, null, null);
        if (commit_old != null) {
            const wrapper: *CommitHookWrapper = @ptrCast(@alignCast(commit_old));
            wrapper.destroy();
        }

        const rollback_old = c.sqlite3_rollback_hook(self.handle, null, null);
        if (rollback_old != null) {
            const wrapper: *RollbackHookWrapper = @ptrCast(@alignCast(rollback_old));
            wrapper.destroy();
        }

        const update_old = c.sqlite3_update_hook(self.handle, null, null);
        if (update_old != null) {
            const wrapper: *UpdateHookWrapper = @ptrCast(@alignCast(update_old));
            wrapper.destroy();
        }
    }
};

/// Flags for opening a database
pub const OpenFlags = struct {
    read_only: bool = false,
    read_write: bool = true,
    create: bool = true,
    uri: bool = false,
    memory: bool = false,
    no_mutex: bool = false,
    full_mutex: bool = false,

    fn toInt(self: OpenFlags) c_int {
        var flags: c_int = 0;
        if (self.read_only) flags |= c.SQLITE_OPEN_READONLY;
        if (self.read_write) flags |= c.SQLITE_OPEN_READWRITE;
        if (self.create) flags |= c.SQLITE_OPEN_CREATE;
        if (self.uri) flags |= c.SQLITE_OPEN_URI;
        if (self.memory) flags |= c.SQLITE_OPEN_MEMORY;
        if (self.no_mutex) flags |= c.SQLITE_OPEN_NOMUTEX;
        if (self.full_mutex) flags |= c.SQLITE_OPEN_FULLMUTEX;
        return flags;
    }
};

/// Prepared SQL statement
pub const Statement = struct {
    handle: ?*c.sqlite3_stmt,
    db: *Database,

    const Self = @This();

    /// Finalizes (destroys) the statement.
    pub fn finalize(self: *Self) void {
        if (self.handle) |h| {
            _ = c.sqlite3_finalize(h);
            self.handle = null;
        }
    }

    /// Resets the statement for re-execution.
    pub fn reset(self: *Self) Error!void {
        const result = c.sqlite3_reset(self.handle);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Clears all parameter bindings.
    pub fn clearBindings(self: *Self) Error!void {
        const result = c.sqlite3_clear_bindings(self.handle);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    // ========================================================================
    // Statement metadata
    // ========================================================================

    /// Returns the SQL text of the statement.
    pub fn sql(self: *Self) ?[]const u8 {
        const s = c.sqlite3_sql(self.handle);
        if (s) |ptr| {
            return std.mem.span(ptr);
        }
        return null;
    }

    /// Returns whether the statement is read-only.
    pub fn isReadOnly(self: *Self) bool {
        return c.sqlite3_stmt_readonly(self.handle) != 0;
    }

    /// Returns the number of parameters in the statement.
    pub fn parameterCount(self: *Self) u32 {
        return @intCast(c.sqlite3_bind_parameter_count(self.handle));
    }

    /// Returns the index of a named parameter.
    ///
    /// Supports :name, @name, and $name styles.
    /// Returns null if the parameter is not found.
    pub fn parameterIndex(self: *Self, name: [:0]const u8) ?u32 {
        const idx = c.sqlite3_bind_parameter_index(self.handle, name.ptr);
        if (idx == 0) return null;
        return @intCast(idx);
    }

    /// Returns the name of a parameter by index.
    ///
    /// Returns null if the parameter has no name (positional ?).
    pub fn parameterName(self: *Self, index: u32) ?[]const u8 {
        const name = c.sqlite3_bind_parameter_name(self.handle, @intCast(index));
        if (name) |n| {
            return std.mem.span(n);
        }
        return null;
    }

    // ========================================================================
    // Bind parameters (1-indexed as per SQLite convention)
    // ========================================================================

    /// Binds NULL to a parameter.
    pub fn bindNull(self: *Self, index: u32) Error!void {
        const result = c.sqlite3_bind_null(self.handle, @intCast(index));
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Binds an integer to a parameter.
    pub fn bindInt(self: *Self, index: u32, value: i64) Error!void {
        const result = c.sqlite3_bind_int64(self.handle, @intCast(index), value);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Binds a float to a parameter.
    pub fn bindFloat(self: *Self, index: u32, value: f64) Error!void {
        const result = c.sqlite3_bind_double(self.handle, @intCast(index), value);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Binds text to a parameter.
    /// The text is copied by SQLite (SQLITE_TRANSIENT).
    pub fn bindText(self: *Self, index: u32, value: []const u8) Error!void {
        const result = c.sqlite3_bind_text(
            self.handle,
            @intCast(index),
            value.ptr,
            @intCast(value.len),
            c.SQLITE_TRANSIENT,
        );
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Binds a blob to a parameter.
    /// The blob is copied by SQLite (SQLITE_TRANSIENT).
    pub fn bindBlob(self: *Self, index: u32, value: []const u8) Error!void {
        const result = c.sqlite3_bind_blob(
            self.handle,
            @intCast(index),
            value.ptr,
            @intCast(value.len),
            c.SQLITE_TRANSIENT,
        );
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Binds a boolean to a parameter (as integer 0 or 1).
    pub fn bindBool(self: *Self, index: u32, value: bool) Error!void {
        try self.bindInt(index, if (value) 1 else 0);
    }

    /// Binds a zeroblob (a blob of zeros) to a parameter.
    ///
    /// Useful for reserving space for blob data that will be written later.
    pub fn bindZeroblob(self: *Self, index: u32, size: u32) Error!void {
        const result = c.sqlite3_bind_zeroblob(self.handle, @intCast(index), @intCast(size));
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    // ========================================================================
    // Named parameter binding
    // ========================================================================

    /// Binds NULL to a named parameter.
    pub fn bindNullNamed(self: *Self, name: [:0]const u8) Error!void {
        const idx = self.parameterIndex(name) orelse return Error.Range;
        try self.bindNull(idx);
    }

    /// Binds an integer to a named parameter.
    pub fn bindIntNamed(self: *Self, name: [:0]const u8, value: i64) Error!void {
        const idx = self.parameterIndex(name) orelse return Error.Range;
        try self.bindInt(idx, value);
    }

    /// Binds a float to a named parameter.
    pub fn bindFloatNamed(self: *Self, name: [:0]const u8, value: f64) Error!void {
        const idx = self.parameterIndex(name) orelse return Error.Range;
        try self.bindFloat(idx, value);
    }

    /// Binds text to a named parameter.
    pub fn bindTextNamed(self: *Self, name: [:0]const u8, value: []const u8) Error!void {
        const idx = self.parameterIndex(name) orelse return Error.Range;
        try self.bindText(idx, value);
    }

    /// Binds a blob to a named parameter.
    pub fn bindBlobNamed(self: *Self, name: [:0]const u8, value: []const u8) Error!void {
        const idx = self.parameterIndex(name) orelse return Error.Range;
        try self.bindBlob(idx, value);
    }

    /// Binds a boolean to a named parameter.
    pub fn bindBoolNamed(self: *Self, name: [:0]const u8, value: bool) Error!void {
        const idx = self.parameterIndex(name) orelse return Error.Range;
        try self.bindBool(idx, value);
    }

    // ========================================================================
    // Execution
    // ========================================================================

    /// Executes one step of the statement.
    ///
    /// Returns true if there's a row available (for SELECT statements).
    /// Returns false when done (SQLITE_DONE).
    pub fn step(self: *Self) Error!bool {
        const result = c.sqlite3_step(self.handle);
        return switch (result) {
            c.SQLITE_ROW => true,
            c.SQLITE_DONE => false,
            else => resultToError(result),
        };
    }

    // ========================================================================
    // Column access (0-indexed as per SQLite convention)
    // ========================================================================

    /// Returns the number of columns in the result set.
    pub fn columnCount(self: *Self) u32 {
        return @intCast(c.sqlite3_column_count(self.handle));
    }

    /// Returns the name of a column.
    pub fn columnName(self: *Self, index: u32) ?[]const u8 {
        const name = c.sqlite3_column_name(self.handle, @intCast(index));
        if (name) |n| {
            return std.mem.span(n);
        }
        return null;
    }

    /// Returns the type of a column value.
    pub fn columnType(self: *Self, index: u32) ColumnType {
        const col_type = c.sqlite3_column_type(self.handle, @intCast(index));
        return switch (col_type) {
            c.SQLITE_INTEGER => .integer,
            c.SQLITE_FLOAT => .float,
            c.SQLITE_TEXT => .text,
            c.SQLITE_BLOB => .blob,
            c.SQLITE_NULL => .null_value,
            else => .null_value,
        };
    }

    /// Returns an integer column value.
    pub fn columnInt(self: *Self, index: u32) i64 {
        return c.sqlite3_column_int64(self.handle, @intCast(index));
    }

    /// Returns a float column value.
    pub fn columnFloat(self: *Self, index: u32) f64 {
        return c.sqlite3_column_double(self.handle, @intCast(index));
    }

    /// Returns a text column value.
    pub fn columnText(self: *Self, index: u32) ?[]const u8 {
        const len = c.sqlite3_column_bytes(self.handle, @intCast(index));
        const text = c.sqlite3_column_text(self.handle, @intCast(index));
        if (text) |t| {
            return t[0..@intCast(len)];
        }
        return null;
    }

    /// Returns a blob column value.
    pub fn columnBlob(self: *Self, index: u32) ?[]const u8 {
        const len = c.sqlite3_column_bytes(self.handle, @intCast(index));
        const blob = c.sqlite3_column_blob(self.handle, @intCast(index));
        if (blob) |b| {
            const ptr: [*]const u8 = @ptrCast(b);
            return ptr[0..@intCast(len)];
        }
        return null;
    }

    /// Returns true if the column is NULL.
    pub fn columnIsNull(self: *Self, index: u32) bool {
        return self.columnType(index) == .null_value;
    }

    /// Returns a boolean column value (interprets 0 as false, non-zero as true).
    pub fn columnBool(self: *Self, index: u32) bool {
        return self.columnInt(index) != 0;
    }

    /// Returns the size in bytes of a blob or text column.
    pub fn columnBytes(self: *Self, index: u32) u32 {
        return @intCast(c.sqlite3_column_bytes(self.handle, @intCast(index)));
    }

    /// Returns the declared type of a column.
    ///
    /// This is the type declared in the CREATE TABLE statement.
    pub fn columnDeclType(self: *Self, index: u32) ?[]const u8 {
        const dtype = c.sqlite3_column_decltype(self.handle, @intCast(index));
        if (dtype) |d| {
            return std.mem.span(d);
        }
        return null;
    }
};

// ============================================================================
// Backup API
// ============================================================================

/// SQLite online backup handle.
///
/// Allows copying database content from one database to another while
/// both databases are in use. Useful for:
/// - Creating database backups
/// - Copying in-memory databases to files
/// - Copying file databases to memory
pub const Backup = struct {
    handle: ?*c.sqlite3_backup,
    dest_db: *Database,
    source_db: *Database,

    const Self = @This();

    /// Initializes a backup from source to destination database.
    ///
    /// The `dest_name` and `source_name` are usually "main" for the main database.
    /// Use "temp" for temporary databases.
    pub fn init(
        dest_db: *Database,
        dest_name: [:0]const u8,
        source_db: *Database,
        source_name: [:0]const u8,
    ) Error!Self {
        const handle = c.sqlite3_backup_init(
            dest_db.handle,
            dest_name.ptr,
            source_db.handle,
            source_name.ptr,
        );

        if (handle == null) {
            // Get error from destination database
            const err_code = c.sqlite3_errcode(dest_db.handle);
            return resultToError(err_code);
        }

        return .{
            .handle = handle,
            .dest_db = dest_db,
            .source_db = source_db,
        };
    }

    /// Convenience function to backup the main database.
    pub fn initMain(dest_db: *Database, source_db: *Database) Error!Self {
        return init(dest_db, "main", source_db, "main");
    }

    /// Copies up to `n_pages` pages from source to destination.
    ///
    /// Use -1 to copy all remaining pages in one call.
    ///
    /// Returns:
    /// - `true` if there are more pages to copy
    /// - `false` if the backup is complete
    pub fn step(self: *Self, n_pages: i32) Error!bool {
        const result = c.sqlite3_backup_step(self.handle, n_pages);
        return switch (result) {
            c.SQLITE_OK => true,
            c.SQLITE_DONE => false,
            c.SQLITE_BUSY, c.SQLITE_LOCKED => Error.Busy,
            else => resultToError(result),
        };
    }

    /// Copies all remaining pages in one call.
    ///
    /// Equivalent to `step(-1)`.
    pub fn stepAll(self: *Self) Error!void {
        _ = try self.step(-1);
    }

    /// Returns the number of pages still to be copied.
    pub fn remaining(self: *Self) i32 {
        return c.sqlite3_backup_remaining(self.handle);
    }

    /// Returns the total number of pages in the source database.
    pub fn pageCount(self: *Self) i32 {
        return c.sqlite3_backup_pagecount(self.handle);
    }

    /// Returns the progress as a percentage (0-100).
    pub fn progress(self: *Self) u8 {
        const total = self.pageCount();
        if (total == 0) return 100;
        const done = total - self.remaining();
        return @intCast(@divFloor(done * 100, total));
    }

    /// Finishes the backup operation and releases resources.
    ///
    /// Must be called when done with the backup, even if an error occurred.
    pub fn finish(self: *Self) Error!void {
        if (self.handle) |h| {
            const result = c.sqlite3_backup_finish(h);
            self.handle = null;
            if (result != c.SQLITE_OK) {
                return resultToError(result);
            }
        }
    }

    /// Alias for finish() for RAII-style usage.
    pub fn deinit(self: *Self) void {
        self.finish() catch {};
    }
};

/// Copies an entire database to another database.
///
/// This is a convenience function that performs a complete backup in one call.
/// For large databases, consider using Backup directly with step() for progress reporting.
pub fn backupDatabase(dest_db: *Database, source_db: *Database) Error!void {
    var backup = try Backup.initMain(dest_db, source_db);
    defer backup.deinit();
    try backup.stepAll();
}

/// Copies a database to a file.
///
/// Creates a new file (or overwrites existing) with the database contents.
pub fn backupToFile(source_db: *Database, path: [:0]const u8) Error!void {
    var dest_db = try Database.open(path);
    defer dest_db.close();
    try backupDatabase(&dest_db, source_db);
}

/// Loads a database from a file into memory.
///
/// Returns a new in-memory database with the file contents.
pub fn loadFromFile(path: [:0]const u8) Error!Database {
    var file_db = try Database.open(path);
    defer file_db.close();

    var mem_db = try openMemory();
    errdefer mem_db.close();

    try backupDatabase(&mem_db, &file_db);
    return mem_db;
}

/// Column data types
pub const ColumnType = enum {
    integer,
    float,
    text,
    blob,
    null_value,
};

// ============================================================================
// User-Defined Functions (Scalar)
// ============================================================================

/// Context for user-defined function results.
///
/// Used to set the return value of a scalar function.
pub const FunctionContext = struct {
    ctx: *c.sqlite3_context,

    const Self = @This();

    /// Sets the result to NULL.
    pub fn setNull(self: Self) void {
        c.sqlite3_result_null(self.ctx);
    }

    /// Sets the result to an integer.
    pub fn setInt(self: Self, value: i64) void {
        c.sqlite3_result_int64(self.ctx, value);
    }

    /// Sets the result to a float.
    pub fn setFloat(self: Self, value: f64) void {
        c.sqlite3_result_double(self.ctx, value);
    }

    /// Sets the result to text.
    pub fn setText(self: Self, value: []const u8) void {
        c.sqlite3_result_text(
            self.ctx,
            value.ptr,
            @intCast(value.len),
            c.SQLITE_TRANSIENT,
        );
    }

    /// Sets the result to a blob.
    pub fn setBlob(self: Self, value: []const u8) void {
        c.sqlite3_result_blob(
            self.ctx,
            value.ptr,
            @intCast(value.len),
            c.SQLITE_TRANSIENT,
        );
    }

    /// Sets the result to an error.
    pub fn setError(self: Self, msg: []const u8) void {
        c.sqlite3_result_error(self.ctx, msg.ptr, @intCast(msg.len));
    }
};

/// Value passed to user-defined functions.
///
/// Represents an argument or result value.
pub const FunctionValue = struct {
    value: *c.sqlite3_value,

    const Self = @This();

    /// Returns the type of the value.
    pub fn getType(self: Self) ColumnType {
        const vtype = c.sqlite3_value_type(self.value);
        return switch (vtype) {
            c.SQLITE_INTEGER => .integer,
            c.SQLITE_FLOAT => .float,
            c.SQLITE_TEXT => .text,
            c.SQLITE_BLOB => .blob,
            c.SQLITE_NULL => .null_value,
            else => .null_value,
        };
    }

    /// Returns true if the value is NULL.
    pub fn isNull(self: Self) bool {
        return self.getType() == .null_value;
    }

    /// Returns the value as an integer.
    pub fn asInt(self: Self) i64 {
        return c.sqlite3_value_int64(self.value);
    }

    /// Returns the value as a float.
    pub fn asFloat(self: Self) f64 {
        return c.sqlite3_value_double(self.value);
    }

    /// Returns the value as text.
    pub fn asText(self: Self) ?[]const u8 {
        const len = c.sqlite3_value_bytes(self.value);
        const text = c.sqlite3_value_text(self.value);
        if (text) |t| {
            return t[0..@intCast(len)];
        }
        return null;
    }

    /// Returns the value as a blob.
    pub fn asBlob(self: Self) ?[]const u8 {
        const len = c.sqlite3_value_bytes(self.value);
        const blob = c.sqlite3_value_blob(self.value);
        if (blob) |b| {
            const ptr: [*]const u8 = @ptrCast(b);
            return ptr[0..@intCast(len)];
        }
        return null;
    }
};

/// Type signature for scalar function callbacks.
pub const ScalarFn = *const fn (ctx: FunctionContext, args: []const FunctionValue) void;

/// Wrapper data structure stored in SQLite.
/// Uses page allocator for simplicity since functions are typically registered once.
const ScalarFnWrapper = struct {
    func: ScalarFn,

    fn create(func: ScalarFn) !*ScalarFnWrapper {
        const wrapper = try std.heap.page_allocator.create(ScalarFnWrapper);
        wrapper.func = func;
        return wrapper;
    }

    fn destroy(self: *ScalarFnWrapper) void {
        std.heap.page_allocator.destroy(self);
    }
};

/// C callback trampoline for scalar functions.
fn scalarCallback(
    ctx: ?*c.sqlite3_context,
    argc: c_int,
    argv: [*c]?*c.sqlite3_value,
) callconv(.c) void {
    const user_data = c.sqlite3_user_data(ctx);
    if (user_data == null) return;

    const wrapper: *ScalarFnWrapper = @ptrCast(@alignCast(user_data));
    const func_ctx = FunctionContext{ .ctx = ctx.? };

    // Build args array
    const args_count: usize = @intCast(argc);
    var args: [16]FunctionValue = undefined; // Max 16 args
    const actual_count = @min(args_count, 16);

    for (0..actual_count) |i| {
        if (argv[i]) |v| {
            args[i] = FunctionValue{ .value = v };
        }
    }

    wrapper.func(func_ctx, args[0..actual_count]);
}

/// Destructor callback for function user data.
fn scalarDestructor(ptr: ?*anyopaque) callconv(.c) void {
    if (ptr) |p| {
        const wrapper: *ScalarFnWrapper = @ptrCast(@alignCast(p));
        wrapper.destroy();
    }
}

// ============================================================================
// Aggregate Functions
// ============================================================================

/// Type signature for aggregate step function.
/// Called once for each row in the aggregate.
pub const AggregateStepFn = *const fn (ctx: AggregateContext, args: []const FunctionValue) void;

/// Type signature for aggregate final function.
/// Called once at the end to produce the result.
pub const AggregateFinalFn = *const fn (ctx: AggregateContext) void;

/// Context for aggregate functions.
///
/// Provides access to aggregate-specific memory that persists
/// across all step() calls for a single aggregate computation.
pub const AggregateContext = struct {
    ctx: *c.sqlite3_context,

    const Self = @This();

    /// Gets the aggregate context memory.
    /// The memory is initialized to zero on first call.
    /// `size` is the number of bytes of context needed.
    pub fn getAggregateContext(self: Self, comptime T: type) ?*T {
        const ptr = c.sqlite3_aggregate_context(self.ctx, @sizeOf(T));
        if (ptr == null) return null;
        return @ptrCast(@alignCast(ptr));
    }

    /// Sets the result to NULL.
    pub fn setNull(self: Self) void {
        c.sqlite3_result_null(self.ctx);
    }

    /// Sets the result to an integer.
    pub fn setInt(self: Self, value: i64) void {
        c.sqlite3_result_int64(self.ctx, value);
    }

    /// Sets the result to a float.
    pub fn setFloat(self: Self, value: f64) void {
        c.sqlite3_result_double(self.ctx, value);
    }

    /// Sets the result to text.
    pub fn setText(self: Self, value: []const u8) void {
        c.sqlite3_result_text(
            self.ctx,
            value.ptr,
            @intCast(value.len),
            c.SQLITE_TRANSIENT,
        );
    }

    /// Sets the result to a blob.
    pub fn setBlob(self: Self, value: []const u8) void {
        c.sqlite3_result_blob(
            self.ctx,
            value.ptr,
            @intCast(value.len),
            c.SQLITE_TRANSIENT,
        );
    }

    /// Sets the result to an error.
    pub fn setError(self: Self, msg: []const u8) void {
        c.sqlite3_result_error(self.ctx, msg.ptr, @intCast(msg.len));
    }
};

/// Wrapper for aggregate function pair.
const AggregateFnWrapper = struct {
    step_fn: AggregateStepFn,
    final_fn: AggregateFinalFn,

    fn create(step_fn: AggregateStepFn, final_fn: AggregateFinalFn) !*AggregateFnWrapper {
        const wrapper = try std.heap.page_allocator.create(AggregateFnWrapper);
        wrapper.step_fn = step_fn;
        wrapper.final_fn = final_fn;
        return wrapper;
    }

    fn destroy(self: *AggregateFnWrapper) void {
        std.heap.page_allocator.destroy(self);
    }
};

/// C callback trampoline for aggregate step function.
fn aggregateStepCallback(
    ctx: ?*c.sqlite3_context,
    argc: c_int,
    argv: [*c]?*c.sqlite3_value,
) callconv(.c) void {
    const user_data = c.sqlite3_user_data(ctx);
    if (user_data == null) return;

    const wrapper: *AggregateFnWrapper = @ptrCast(@alignCast(user_data));
    const agg_ctx = AggregateContext{ .ctx = ctx.? };

    // Build args array
    const args_count: usize = @intCast(argc);
    var args: [16]FunctionValue = undefined; // Max 16 args
    const actual_count = @min(args_count, 16);

    for (0..actual_count) |i| {
        if (argv[i]) |v| {
            args[i] = FunctionValue{ .value = v };
        }
    }

    wrapper.step_fn(agg_ctx, args[0..actual_count]);
}

/// C callback trampoline for aggregate final function.
fn aggregateFinalCallback(ctx: ?*c.sqlite3_context) callconv(.c) void {
    const user_data = c.sqlite3_user_data(ctx);
    if (user_data == null) return;

    const wrapper: *AggregateFnWrapper = @ptrCast(@alignCast(user_data));
    const agg_ctx = AggregateContext{ .ctx = ctx.? };

    wrapper.final_fn(agg_ctx);
}

/// Destructor callback for aggregate function user data.
fn aggregateDestructor(ptr: ?*anyopaque) callconv(.c) void {
    if (ptr) |p| {
        const wrapper: *AggregateFnWrapper = @ptrCast(@alignCast(p));
        wrapper.destroy();
    }
}

// ============================================================================
// Custom Collations
// ============================================================================

/// Type signature for collation comparison functions.
///
/// Should return:
/// - negative value if a < b
/// - zero if a == b
/// - positive value if a > b
pub const CollationFn = *const fn (a: []const u8, b: []const u8) i32;

/// Wrapper for collation function.
const CollationWrapper = struct {
    func: CollationFn,

    fn create(func: CollationFn) !*CollationWrapper {
        const wrapper = try std.heap.page_allocator.create(CollationWrapper);
        wrapper.func = func;
        return wrapper;
    }

    fn destroy(self: *CollationWrapper) void {
        std.heap.page_allocator.destroy(self);
    }
};

/// C callback trampoline for collation functions.
fn collationCallback(
    user_data: ?*anyopaque,
    len_a: c_int,
    data_a: ?*const anyopaque,
    len_b: c_int,
    data_b: ?*const anyopaque,
) callconv(.c) c_int {
    if (user_data == null) return 0;

    const wrapper: *CollationWrapper = @ptrCast(@alignCast(user_data));

    const a: []const u8 = if (data_a) |ptr|
        @as([*]const u8, @ptrCast(ptr))[0..@intCast(len_a)]
    else
        "";

    const b: []const u8 = if (data_b) |ptr|
        @as([*]const u8, @ptrCast(ptr))[0..@intCast(len_b)]
    else
        "";

    return wrapper.func(a, b);
}

/// Destructor callback for collation user data.
fn collationDestructor(ptr: ?*anyopaque) callconv(.c) void {
    if (ptr) |p| {
        const wrapper: *CollationWrapper = @ptrCast(@alignCast(p));
        wrapper.destroy();
    }
}

// ============================================================================
// Blob I/O
// ============================================================================

/// Blob handle for incremental I/O operations.
///
/// Allows reading and writing large BLOBs incrementally without
/// loading the entire blob into memory.
pub const Blob = struct {
    handle: ?*c.sqlite3_blob,
    db: *Database,

    const Self = @This();

    /// Opens a blob for incremental I/O.
    ///
    /// Parameters:
    /// - `db`: Database connection
    /// - `schema`: Database name (usually "main")
    /// - `table`: Table name
    /// - `column`: Column name
    /// - `rowid`: Row ID of the blob
    /// - `writable`: If true, opens for read/write; otherwise read-only
    pub fn open(
        db: *Database,
        schema: [:0]const u8,
        table: [:0]const u8,
        column: [:0]const u8,
        rowid: i64,
        writable: bool,
    ) Error!Self {
        var handle: ?*c.sqlite3_blob = null;
        const flags: c_int = if (writable) 1 else 0;

        const result = c.sqlite3_blob_open(
            db.handle,
            schema.ptr,
            table.ptr,
            column.ptr,
            rowid,
            flags,
            &handle,
        );

        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }

        return Self{
            .handle = handle,
            .db = db,
        };
    }

    /// Opens a blob with allocator for runtime strings.
    pub fn openAlloc(
        db: *Database,
        allocator: std.mem.Allocator,
        schema: []const u8,
        table: []const u8,
        column: []const u8,
        rowid: i64,
        writable: bool,
    ) !Self {
        // Create null-terminated copies
        const schema_z = try allocator.dupeZ(u8, schema);
        defer allocator.free(schema_z);

        const table_z = try allocator.dupeZ(u8, table);
        defer allocator.free(table_z);

        const column_z = try allocator.dupeZ(u8, column);
        defer allocator.free(column_z);

        return Self.open(db, schema_z, table_z, column_z, rowid, writable);
    }

    /// Closes the blob handle.
    pub fn close(self: *Self) Error!void {
        if (self.handle) |h| {
            const result = c.sqlite3_blob_close(h);
            self.handle = null;
            if (result != c.SQLITE_OK) {
                return resultToError(result);
            }
        }
    }

    /// Closes the blob handle without checking for errors.
    pub fn deinit(self: *Self) void {
        if (self.handle) |h| {
            _ = c.sqlite3_blob_close(h);
            self.handle = null;
        }
    }

    /// Returns the size of the blob in bytes.
    pub fn bytes(self: *Self) i32 {
        if (self.handle) |h| {
            return c.sqlite3_blob_bytes(h);
        }
        return 0;
    }

    /// Reads data from the blob.
    ///
    /// Parameters:
    /// - `buffer`: Buffer to read into
    /// - `offset`: Offset within the blob to start reading
    pub fn read(self: *Self, buffer: []u8, offset: i32) Error!void {
        if (self.handle == null) return error.SqliteError;

        const result = c.sqlite3_blob_read(
            self.handle,
            buffer.ptr,
            @intCast(buffer.len),
            offset,
        );

        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Writes data to the blob.
    ///
    /// Parameters:
    /// - `data`: Data to write
    /// - `offset`: Offset within the blob to start writing
    ///
    /// Note: This cannot change the size of the blob. Use UPDATE to resize.
    pub fn write(self: *Self, data: []const u8, offset: i32) Error!void {
        if (self.handle == null) return error.SqliteError;

        const result = c.sqlite3_blob_write(
            self.handle,
            data.ptr,
            @intCast(data.len),
            offset,
        );

        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Moves the blob handle to a different row.
    ///
    /// This allows reusing an open blob handle for a different row
    /// in the same table, which is faster than closing and reopening.
    pub fn reopen(self: *Self, rowid: i64) Error!void {
        if (self.handle == null) return error.SqliteError;

        const result = c.sqlite3_blob_reopen(self.handle, rowid);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Reads the entire blob into a newly allocated buffer.
    pub fn readAll(self: *Self, allocator: std.mem.Allocator) ![]u8 {
        const size = self.bytes();
        if (size <= 0) return &[_]u8{};

        const buffer = try allocator.alloc(u8, @intCast(size));
        errdefer allocator.free(buffer);

        try self.read(buffer, 0);
        return buffer;
    }
};

// ============================================================================
// Hooks (Commit, Rollback, Update)
// ============================================================================

/// Type signature for commit hook callback.
/// Return 0 to allow the commit, non-zero to force rollback.
pub const CommitHookFn = *const fn (user_data: ?*anyopaque) i32;

/// Type signature for rollback hook callback.
pub const RollbackHookFn = *const fn (user_data: ?*anyopaque) void;

/// Type signature for update hook callback.
/// Parameters are: operation (INSERT/UPDATE/DELETE), database name, table name, rowid
pub const UpdateHookFn = *const fn (
    user_data: ?*anyopaque,
    operation: i32,
    db_name: [*:0]const u8,
    table_name: [*:0]const u8,
    rowid: i64,
) void;

/// Update operation types for update hooks.
pub const UpdateOperation = enum(i32) {
    insert = c.SQLITE_INSERT,
    update = c.SQLITE_UPDATE,
    delete = c.SQLITE_DELETE,

    pub fn fromInt(value: i32) ?UpdateOperation {
        return switch (value) {
            c.SQLITE_INSERT => .insert,
            c.SQLITE_UPDATE => .update,
            c.SQLITE_DELETE => .delete,
            else => null,
        };
    }
};

/// Zig-friendly commit hook callback type.
pub const ZigCommitHookFn = *const fn () bool;

/// Zig-friendly rollback hook callback type.
pub const ZigRollbackHookFn = *const fn () void;

/// Zig-friendly update hook callback type.
pub const ZigUpdateHookFn = *const fn (operation: UpdateOperation, db_name: []const u8, table_name: []const u8, rowid: i64) void;

/// Wrapper for Zig commit hook.
const CommitHookWrapper = struct {
    func: ZigCommitHookFn,

    fn create(func: ZigCommitHookFn) !*CommitHookWrapper {
        const wrapper = try std.heap.page_allocator.create(CommitHookWrapper);
        wrapper.func = func;
        return wrapper;
    }

    fn destroy(self: *CommitHookWrapper) void {
        std.heap.page_allocator.destroy(self);
    }
};

/// Wrapper for Zig rollback hook.
const RollbackHookWrapper = struct {
    func: ZigRollbackHookFn,

    fn create(func: ZigRollbackHookFn) !*RollbackHookWrapper {
        const wrapper = try std.heap.page_allocator.create(RollbackHookWrapper);
        wrapper.func = func;
        return wrapper;
    }

    fn destroy(self: *RollbackHookWrapper) void {
        std.heap.page_allocator.destroy(self);
    }
};

/// Wrapper for Zig update hook.
const UpdateHookWrapper = struct {
    func: ZigUpdateHookFn,

    fn create(func: ZigUpdateHookFn) !*UpdateHookWrapper {
        const wrapper = try std.heap.page_allocator.create(UpdateHookWrapper);
        wrapper.func = func;
        return wrapper;
    }

    fn destroy(self: *UpdateHookWrapper) void {
        std.heap.page_allocator.destroy(self);
    }
};

/// C callback trampoline for commit hooks.
fn commitHookCallback(user_data: ?*anyopaque) callconv(.c) c_int {
    if (user_data == null) return 0;

    const wrapper: *CommitHookWrapper = @ptrCast(@alignCast(user_data));
    const allow_commit = wrapper.func();
    return if (allow_commit) 0 else 1;
}

/// C callback trampoline for rollback hooks.
fn rollbackHookCallback(user_data: ?*anyopaque) callconv(.c) void {
    if (user_data == null) return;

    const wrapper: *RollbackHookWrapper = @ptrCast(@alignCast(user_data));
    wrapper.func();
}

/// C callback trampoline for update hooks.
fn updateHookCallback(
    user_data: ?*anyopaque,
    operation: c_int,
    db_name: [*c]const u8,
    table_name: [*c]const u8,
    rowid: c.sqlite3_int64,
) callconv(.c) void {
    if (user_data == null) return;

    const wrapper: *UpdateHookWrapper = @ptrCast(@alignCast(user_data));
    const op = UpdateOperation.fromInt(operation) orelse return;
    const db_str = std.mem.span(db_name);
    const table_str = std.mem.span(table_name);

    wrapper.func(op, db_str, table_str, rowid);
}

// ============================================================================
// Convenience functions
// ============================================================================

/// Opens a database connection.
pub fn open(path: [:0]const u8) Error!Database {
    return Database.open(path);
}

/// Opens an in-memory database.
pub fn openMemory() Error!Database {
    return Database.open(":memory:");
}

/// Returns the SQLite version string.
pub fn version() []const u8 {
    return std.mem.span(c.sqlite3_libversion());
}

/// Returns the SQLite version number.
pub fn versionNumber() i32 {
    return c.sqlite3_libversion_number();
}

// ============================================================================
// Tests
// ============================================================================

test "version" {
    const ver = version();
    try std.testing.expect(ver.len > 0);
    // Should start with "3."
    try std.testing.expectEqualStrings("3.", ver[0..2]);
}

test "open and close memory database" {
    var db = try openMemory();
    defer db.close();
    try std.testing.expect(db.handle != null);
}

test "create table and insert" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)");
    try db.exec("INSERT INTO test (name) VALUES ('hello')");

    try std.testing.expectEqual(@as(i64, 1), db.lastInsertRowId());
    try std.testing.expectEqual(@as(i32, 1), db.changes());
}

test "prepared statement with binding" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");

    var stmt = try db.prepare("INSERT INTO users (name, age) VALUES (?, ?)");
    defer stmt.finalize();

    try stmt.bindText(1, "Alice");
    try stmt.bindInt(2, 30);
    _ = try stmt.step();

    try stmt.reset();
    try stmt.clearBindings();

    try stmt.bindText(1, "Bob");
    try stmt.bindInt(2, 25);
    _ = try stmt.step();

    try std.testing.expectEqual(@as(i64, 2), db.lastInsertRowId());
}

test "select query" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
    try db.exec("INSERT INTO users (name) VALUES ('Alice'), ('Bob')");

    var stmt = try db.prepare("SELECT id, name FROM users ORDER BY id");
    defer stmt.finalize();

    // First row
    try std.testing.expect(try stmt.step());
    try std.testing.expectEqual(@as(i64, 1), stmt.columnInt(0));
    try std.testing.expectEqualStrings("Alice", stmt.columnText(1).?);

    // Second row
    try std.testing.expect(try stmt.step());
    try std.testing.expectEqual(@as(i64, 2), stmt.columnInt(0));
    try std.testing.expectEqualStrings("Bob", stmt.columnText(1).?);

    // No more rows
    try std.testing.expect(!try stmt.step());
}

test "transaction commit" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (x INTEGER)");

    try db.begin();
    try db.exec("INSERT INTO test VALUES (1)");
    try db.exec("INSERT INTO test VALUES (2)");
    try db.commit();

    var stmt = try db.prepare("SELECT COUNT(*) FROM test");
    defer stmt.finalize();
    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 2), stmt.columnInt(0));
}

test "transaction rollback" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (x INTEGER)");
    try db.exec("INSERT INTO test VALUES (1)");

    try db.begin();
    try db.exec("INSERT INTO test VALUES (2)");
    try db.rollback();

    var stmt = try db.prepare("SELECT COUNT(*) FROM test");
    defer stmt.finalize();
    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 1), stmt.columnInt(0));
}

test "foreign keys" {
    var db = try openMemory();
    defer db.close();

    try db.setForeignKeys(true);

    try db.exec(
        \\CREATE TABLE parent (id INTEGER PRIMARY KEY);
        \\CREATE TABLE child (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id));
    );

    try db.exec("INSERT INTO parent VALUES (1)");
    try db.exec("INSERT INTO child VALUES (1, 1)");

    // This should fail due to FK constraint
    const result = db.exec("INSERT INTO child VALUES (2, 999)");
    try std.testing.expectError(Error.Constraint, result);
}

test "null values" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (x INTEGER, y TEXT)");

    var stmt = try db.prepare("INSERT INTO test VALUES (?, ?)");
    defer stmt.finalize();

    try stmt.bindInt(1, 42);
    try stmt.bindNull(2);
    _ = try stmt.step();

    var query = try db.prepare("SELECT x, y FROM test");
    defer query.finalize();

    _ = try query.step();
    try std.testing.expectEqual(@as(i64, 42), query.columnInt(0));
    try std.testing.expect(query.columnIsNull(1));
}

test "blob data" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (data BLOB)");

    const blob_data = [_]u8{ 0x00, 0x01, 0x02, 0xFF, 0xFE };

    var stmt = try db.prepare("INSERT INTO test VALUES (?)");
    defer stmt.finalize();
    try stmt.bindBlob(1, &blob_data);
    _ = try stmt.step();

    var query = try db.prepare("SELECT data FROM test");
    defer query.finalize();
    _ = try query.step();

    const result = query.columnBlob(0).?;
    try std.testing.expectEqualSlices(u8, &blob_data, result);
}

test "named parameters" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");

    // Test :name style
    var stmt = try db.prepare("INSERT INTO users (name, age) VALUES (:name, :age)");
    defer stmt.finalize();

    try std.testing.expectEqual(@as(u32, 2), stmt.parameterCount());
    try std.testing.expectEqual(@as(?u32, 1), stmt.parameterIndex(":name"));
    try std.testing.expectEqual(@as(?u32, 2), stmt.parameterIndex(":age"));
    try std.testing.expectEqualStrings(":name", stmt.parameterName(1).?);
    try std.testing.expectEqualStrings(":age", stmt.parameterName(2).?);

    try stmt.bindTextNamed(":name", "Alice");
    try stmt.bindIntNamed(":age", 30);
    _ = try stmt.step();

    try std.testing.expectEqual(@as(i64, 1), db.lastInsertRowId());
}

test "savepoint" {
    const allocator = std.testing.allocator;
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (x INTEGER)");
    try db.exec("INSERT INTO test VALUES (1)");

    try db.begin();

    // Create savepoint, insert, then rollback to it
    try db.savepoint(allocator, "sp1");
    try db.exec("INSERT INTO test VALUES (2)");
    try db.rollbackTo(allocator, "sp1");

    // Insert another value
    try db.exec("INSERT INTO test VALUES (3)");
    try db.release(allocator, "sp1");

    try db.commit();

    // Should have rows 1 and 3, not 2
    var stmt = try db.prepare("SELECT x FROM test ORDER BY x");
    defer stmt.finalize();

    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 1), stmt.columnInt(0));
    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 3), stmt.columnInt(0));
    try std.testing.expect(!try stmt.step());
}

test "busy timeout" {
    var db = try openMemory();
    defer db.close();

    try db.setBusyTimeout(5000);
    // If we get here without error, the function works
}

test "statement metadata" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)");

    var select = try db.prepare("SELECT id, name FROM test");
    defer select.finalize();

    try std.testing.expect(select.isReadOnly());
    try std.testing.expectEqualStrings("SELECT id, name FROM test", select.sql().?);

    var insert = try db.prepare("INSERT INTO test (name) VALUES (?)");
    defer insert.finalize();

    try std.testing.expect(!insert.isReadOnly());
}

test "boolean bind and column" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (flag INTEGER)");

    var stmt = try db.prepare("INSERT INTO test VALUES (?)");
    defer stmt.finalize();

    try stmt.bindBool(1, true);
    _ = try stmt.step();

    try stmt.reset();
    try stmt.bindBool(1, false);
    _ = try stmt.step();

    var query = try db.prepare("SELECT flag FROM test ORDER BY rowid");
    defer query.finalize();

    _ = try query.step();
    try std.testing.expect(query.columnBool(0));

    _ = try query.step();
    try std.testing.expect(!query.columnBool(0));
}

test "backup memory to memory" {
    // Create source database with data
    var source = try openMemory();
    defer source.close();

    try source.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)");
    try source.exec("INSERT INTO test (value) VALUES ('hello'), ('world')");

    // Create destination database
    var dest = try openMemory();
    defer dest.close();

    // Perform backup
    var backup = try Backup.initMain(&dest, &source);
    defer backup.deinit();

    // Copy all pages (page count may be 0 for small in-memory DBs until step is called)
    try backup.stepAll();

    // After stepAll, finish should work
    try backup.finish();

    // Verify data was copied
    var stmt = try dest.prepare("SELECT COUNT(*) FROM test");
    defer stmt.finalize();
    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 2), stmt.columnInt(0));
}

test "backup convenience function" {
    // Create source database with data
    var source = try openMemory();
    defer source.close();

    try source.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
    try source.exec("INSERT INTO users (name) VALUES ('Alice'), ('Bob'), ('Charlie')");

    // Create destination and backup
    var dest = try openMemory();
    defer dest.close();

    try backupDatabase(&dest, &source);

    // Verify
    var stmt = try dest.prepare("SELECT name FROM users ORDER BY id");
    defer stmt.finalize();

    _ = try stmt.step();
    try std.testing.expectEqualStrings("Alice", stmt.columnText(0).?);
    _ = try stmt.step();
    try std.testing.expectEqualStrings("Bob", stmt.columnText(0).?);
    _ = try stmt.step();
    try std.testing.expectEqualStrings("Charlie", stmt.columnText(0).?);
}

test "attach and detach memory database" {
    const allocator = std.testing.allocator;

    var db = try openMemory();
    defer db.close();

    // Attach a second in-memory database
    try db.attachMemory(allocator, "db2");

    // Create table in attached database
    try db.exec("CREATE TABLE db2.items (id INTEGER PRIMARY KEY, name TEXT)");
    try db.exec("INSERT INTO db2.items (name) VALUES ('item1'), ('item2')");

    // Query from attached database (use block to ensure stmt is finalized before detach)
    {
        var stmt = try db.prepare("SELECT COUNT(*) FROM db2.items");
        defer stmt.finalize();
        _ = try stmt.step();
        try std.testing.expectEqual(@as(i64, 2), stmt.columnInt(0));
    }

    // List databases
    const dbs = try db.listDatabases(allocator);
    defer Database.freeDatabaseList(allocator, dbs);

    try std.testing.expect(dbs.len >= 2); // main + db2 (temp may or may not be listed)

    // Detach
    try db.detach(allocator, "db2");

    // After detach, db2.items should not be accessible
    const result = db.prepare("SELECT * FROM db2.items");
    try std.testing.expectError(Error.SqliteError, result);
}

// Test function: doubles an integer
fn doubleInt(ctx: FunctionContext, args: []const FunctionValue) void {
    if (args.len != 1) {
        ctx.setError("double() requires 1 argument");
        return;
    }
    if (args[0].isNull()) {
        ctx.setNull();
        return;
    }
    const value = args[0].asInt();
    ctx.setInt(value * 2);
}

// Test function: concatenates strings
fn concatStrings(ctx: FunctionContext, args: []const FunctionValue) void {
    if (args.len < 2) {
        ctx.setError("concat() requires at least 2 arguments");
        return;
    }

    // Simple concat for 2 args
    const a = args[0].asText() orelse "";
    const b = args[1].asText() orelse "";

    // For testing, just return first arg if both empty, otherwise concat
    if (a.len == 0 and b.len == 0) {
        ctx.setText("");
    } else if (a.len == 0) {
        ctx.setText(b);
    } else if (b.len == 0) {
        ctx.setText(a);
    } else {
        // Can't easily concat without allocator, just return first for test
        ctx.setText(a);
    }
}

test "user-defined scalar function" {
    var db = try openMemory();
    defer db.close();

    // Register double function
    try db.createScalarFunction("double", 1, doubleInt);

    try db.exec("CREATE TABLE test (value INTEGER)");
    try db.exec("INSERT INTO test VALUES (5), (10), (15)");

    // Use the custom function
    var stmt = try db.prepare("SELECT double(value) FROM test ORDER BY value");
    defer stmt.finalize();

    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 10), stmt.columnInt(0)); // 5 * 2

    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 20), stmt.columnInt(0)); // 10 * 2

    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 30), stmt.columnInt(0)); // 15 * 2
}

// Test collation: reverse alphabetical order
fn reverseCollation(a: []const u8, b: []const u8) i32 {
    // Standard comparison but negated for reverse order
    const result = std.mem.order(u8, a, b);
    return switch (result) {
        .lt => 1,
        .gt => -1,
        .eq => 0,
    };
}

test "custom collation" {
    var db = try openMemory();
    defer db.close();

    // Register reverse collation
    try db.createCollation("REVERSE", reverseCollation);

    try db.exec("CREATE TABLE names (name TEXT)");
    try db.exec("INSERT INTO names VALUES ('Alice'), ('Bob'), ('Charlie')");

    // Without collation: alphabetical order
    {
        var stmt = try db.prepare("SELECT name FROM names ORDER BY name");
        defer stmt.finalize();

        _ = try stmt.step();
        try std.testing.expectEqualStrings("Alice", stmt.columnText(0).?);
        _ = try stmt.step();
        try std.testing.expectEqualStrings("Bob", stmt.columnText(0).?);
        _ = try stmt.step();
        try std.testing.expectEqualStrings("Charlie", stmt.columnText(0).?);
    }

    // With REVERSE collation: reverse alphabetical order
    {
        var stmt = try db.prepare("SELECT name FROM names ORDER BY name COLLATE REVERSE");
        defer stmt.finalize();

        _ = try stmt.step();
        try std.testing.expectEqualStrings("Charlie", stmt.columnText(0).?);
        _ = try stmt.step();
        try std.testing.expectEqualStrings("Bob", stmt.columnText(0).?);
        _ = try stmt.step();
        try std.testing.expectEqualStrings("Alice", stmt.columnText(0).?);
    }
}

test "blob incremental I/O" {
    var db = try openMemory();
    defer db.close();

    // Create table with blob column
    try db.exec("CREATE TABLE blobs (id INTEGER PRIMARY KEY, data BLOB)");

    // Insert a blob with zeroblob placeholder
    var insert = try db.prepare("INSERT INTO blobs (data) VALUES (zeroblob(100))");
    defer insert.finalize();
    _ = try insert.step();

    const rowid = db.lastInsertRowId();

    // Open blob for writing
    var blob = try Blob.open(&db, "main", "blobs", "data", rowid, true);
    defer blob.deinit();

    // Check size
    try std.testing.expectEqual(@as(i32, 100), blob.bytes());

    // Write some data
    const write_data = "Hello, Blob World!";
    try blob.write(write_data, 0);

    // Close and reopen for reading
    try blob.close();
    blob = try Blob.open(&db, "main", "blobs", "data", rowid, false);

    // Read back
    var read_buffer: [100]u8 = undefined;
    try blob.read(&read_buffer, 0);

    try std.testing.expectEqualStrings(write_data, read_buffer[0..write_data.len]);
}

test "blob read all" {
    const allocator = std.testing.allocator;

    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE files (id INTEGER PRIMARY KEY, content BLOB)");

    // Insert test data
    var insert = try db.prepare("INSERT INTO files (content) VALUES (?)");
    defer insert.finalize();
    const test_data = "This is test blob content for readAll test";
    try insert.bindBlob(1, test_data);
    _ = try insert.step();

    const rowid = db.lastInsertRowId();

    // Read using blob API
    var blob = try Blob.open(&db, "main", "files", "content", rowid, false);
    defer blob.deinit();

    const data = try blob.readAll(allocator);
    defer allocator.free(data);

    try std.testing.expectEqualStrings(test_data, data);
}

test "blob reopen" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE multi (id INTEGER PRIMARY KEY, data BLOB)");

    // Insert multiple rows
    try db.exec("INSERT INTO multi (data) VALUES (X'0102030405')"); // rowid 1
    try db.exec("INSERT INTO multi (data) VALUES (X'0A0B0C0D0E')"); // rowid 2

    // Open first row
    var blob = try Blob.open(&db, "main", "multi", "data", 1, false);
    defer blob.deinit();

    var buffer: [5]u8 = undefined;
    try blob.read(&buffer, 0);
    try std.testing.expectEqualSlices(u8, &[_]u8{ 1, 2, 3, 4, 5 }, &buffer);

    // Reopen to second row
    try blob.reopen(2);
    try blob.read(&buffer, 0);
    try std.testing.expectEqualSlices(u8, &[_]u8{ 10, 11, 12, 13, 14 }, &buffer);
}

// Thread-local state for hook tests
var commit_count: u32 = 0;
var rollback_count: u32 = 0;
var update_count: u32 = 0;
var last_update_op: ?UpdateOperation = null;

fn testCommitHook() bool {
    commit_count += 1;
    return true; // Allow commit
}

fn testRollbackHook() void {
    rollback_count += 1;
}

fn testUpdateHook(op: UpdateOperation, _: []const u8, _: []const u8, _: i64) void {
    update_count += 1;
    last_update_op = op;
}

test "commit hook" {
    commit_count = 0;

    var db = try openMemory();
    defer db.close();

    try db.setCommitHook(testCommitHook);

    try db.exec("CREATE TABLE test (x INTEGER)");
    try std.testing.expect(commit_count >= 1);

    const before = commit_count;
    try db.begin();
    try db.exec("INSERT INTO test VALUES (1)");
    try db.commit();
    try std.testing.expect(commit_count > before);

    // Remove hook
    try db.setCommitHook(null);
}

test "rollback hook" {
    rollback_count = 0;

    var db = try openMemory();
    defer db.close();

    try db.setRollbackHook(testRollbackHook);

    try db.exec("CREATE TABLE test (x INTEGER)");

    try db.begin();
    try db.exec("INSERT INTO test VALUES (1)");
    try db.rollback();

    try std.testing.expect(rollback_count >= 1);

    // Remove hook
    try db.setRollbackHook(null);
}

test "update hook" {
    update_count = 0;
    last_update_op = null;

    var db = try openMemory();
    defer db.close();

    try db.setUpdateHook(testUpdateHook);

    try db.exec("CREATE TABLE test (x INTEGER)");

    // Insert
    try db.exec("INSERT INTO test VALUES (1)");
    try std.testing.expect(last_update_op == .insert);

    // Update
    try db.exec("UPDATE test SET x = 2 WHERE x = 1");
    try std.testing.expect(last_update_op == .update);

    // Delete
    try db.exec("DELETE FROM test WHERE x = 2");
    try std.testing.expect(last_update_op == .delete);

    try std.testing.expect(update_count >= 3);

    // Clear all hooks
    db.clearHooks();
}

// Aggregate function state for sum of squares
const SumSquaresState = struct {
    total: i64 = 0,
};

fn sumSquaresStep(ctx: AggregateContext, args: []const FunctionValue) void {
    const state = ctx.getAggregateContext(SumSquaresState) orelse return;
    if (args.len > 0 and !args[0].isNull()) {
        const val = args[0].asInt();
        state.total += val * val;
    }
}

fn sumSquaresFinal(ctx: AggregateContext) void {
    const state = ctx.getAggregateContext(SumSquaresState) orelse {
        ctx.setNull();
        return;
    };
    ctx.setInt(state.total);
}

test "aggregate function - sum of squares" {
    var db = try openMemory();
    defer db.close();

    try db.createAggregateFunction("sum_squares", 1, sumSquaresStep, sumSquaresFinal);

    try db.exec("CREATE TABLE numbers (value INTEGER)");
    try db.exec("INSERT INTO numbers VALUES (1), (2), (3), (4), (5)");
    // sum of squares: 1 + 4 + 9 + 16 + 25 = 55

    var stmt = try db.prepare("SELECT sum_squares(value) FROM numbers");
    defer stmt.finalize();

    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 55), stmt.columnInt(0));
}

// Aggregate function for string concatenation
const ConcatState = struct {
    buffer: [256]u8 = undefined,
    len: usize = 0,
    separator_seen: bool = false,
};

fn groupConcatStep(ctx: AggregateContext, args: []const FunctionValue) void {
    const state = ctx.getAggregateContext(ConcatState) orelse return;
    if (args.len > 0) {
        if (args[0].asText()) |text| {
            // Add separator if not first
            if (state.separator_seen and state.len < state.buffer.len - 1) {
                state.buffer[state.len] = ',';
                state.len += 1;
            }
            // Copy text
            const remaining = state.buffer.len - state.len;
            const copy_len = @min(text.len, remaining);
            @memcpy(state.buffer[state.len..][0..copy_len], text[0..copy_len]);
            state.len += copy_len;
            state.separator_seen = true;
        }
    }
}

fn groupConcatFinal(ctx: AggregateContext) void {
    const state = ctx.getAggregateContext(ConcatState) orelse {
        ctx.setNull();
        return;
    };
    if (state.len == 0) {
        ctx.setNull();
    } else {
        ctx.setText(state.buffer[0..state.len]);
    }
}

test "aggregate function - group concat" {
    var db = try openMemory();
    defer db.close();

    try db.createAggregateFunction("my_group_concat", 1, groupConcatStep, groupConcatFinal);

    try db.exec("CREATE TABLE names (name TEXT)");
    try db.exec("INSERT INTO names VALUES ('Alice'), ('Bob'), ('Charlie')");

    var stmt = try db.prepare("SELECT my_group_concat(name) FROM names");
    defer stmt.finalize();

    _ = try stmt.step();
    try std.testing.expectEqualStrings("Alice,Bob,Charlie", stmt.columnText(0).?);
}
