//! SQLite database connection
//!
//! Provides the main Database struct for managing SQLite connections.

const std = @import("std");
const c = @import("c.zig").c;
const errors = @import("errors.zig");
const types = @import("types.zig");
const functions = @import("functions.zig");
const stmt_mod = @import("statement.zig");

const Error = errors.Error;
const resultToError = errors.resultToError;
const OpenFlags = types.OpenFlags;
const Limit = types.Limit;
const AuthAction = types.AuthAction;
const AuthResult = types.AuthResult;
const UpdateOperation = types.UpdateOperation;

// Re-export Statement for prepare() return type
pub const Statement = stmt_mod.Statement;

// Function types
const ScalarFn = functions.ScalarFn;
const AggregateStepFn = functions.AggregateStepFn;
const AggregateFinalFn = functions.AggregateFinalFn;
const WindowValueFn = functions.WindowValueFn;
const WindowInverseFn = functions.WindowInverseFn;
const CollationFn = functions.CollationFn;
const ZigCommitHookFn = functions.ZigCommitHookFn;
const ZigRollbackHookFn = functions.ZigRollbackHookFn;
const ZigUpdateHookFn = functions.ZigUpdateHookFn;
const ZigPreUpdateHookFn = functions.ZigPreUpdateHookFn;
const ZigAuthorizerFn = functions.ZigAuthorizerFn;
const ZigProgressFn = functions.ZigProgressFn;
const ZigBusyHandlerFn = functions.ZigBusyHandlerFn;

// Wrappers
const ScalarFnWrapper = functions.ScalarFnWrapper;
const AggregateFnWrapper = functions.AggregateFnWrapper;
const WindowFnWrapper = functions.WindowFnWrapper;
const CollationWrapper = functions.CollationWrapper;
const CommitHookWrapper = functions.CommitHookWrapper;
const RollbackHookWrapper = functions.RollbackHookWrapper;
const UpdateHookWrapper = functions.UpdateHookWrapper;
const PreUpdateHookWrapper = functions.PreUpdateHookWrapper;
const AuthorizerWrapper = functions.AuthorizerWrapper;
const ProgressWrapper = functions.ProgressWrapper;
const BusyHandlerWrapper = functions.BusyHandlerWrapper;

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

    const Self = @This();

    // ========================================================================
    // Opening and Closing
    // ========================================================================

    /// Opens a database connection.
    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 };
    }

    /// Opens a database using a URI connection string.
    pub fn openUri(uri: [:0]const u8) Error!Self {
        var handle: ?*c.sqlite3 = null;
        const flags = c.SQLITE_OPEN_URI | c.SQLITE_OPEN_READWRITE | c.SQLITE_OPEN_CREATE;
        const result = c.sqlite3_open_v2(uri.ptr, &handle, flags, null);

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

        return .{ .handle = handle };
    }

    /// Opens a database using a URI with an allocator for runtime strings.
    pub fn openUriAlloc(allocator: std.mem.Allocator, uri: []const u8) !Self {
        const uri_z = try allocator.dupeZ(u8, uri);
        defer allocator.free(uri_z);
        return Self.openUri(uri_z);
    }

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

    // ========================================================================
    // SQL Execution
    // ========================================================================

    /// Executes SQL statement(s) without returning results.
    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.
    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);
    }

    // ========================================================================
    // Database Info
    // ========================================================================

    /// 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;
    }

    /// 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.
    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.
    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;
    }

    /// Interrupts a long-running query.
    pub fn interrupt(self: *Self) void {
        c.sqlite3_interrupt(self.handle);
    }

    // ========================================================================
    // Transactions
    // ========================================================================

    /// 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.
    pub fn beginImmediate(self: *Self) Error!void {
        try self.exec("BEGIN IMMEDIATE");
    }

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

    /// 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");
    }

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

    /// Creates a savepoint with the given name.
    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.
    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.
    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.
    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.
    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.
    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 WAL mode with recommended settings.
    pub fn enableWalMode(self: *Self, allocator: std.mem.Allocator) !void {
        try self.setJournalMode(allocator, "WAL");
        try self.setSynchronous(allocator, "NORMAL");
    }

    /// Sets the auto_vacuum mode.
    pub fn setAutoVacuum(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
        const sql = try std.fmt.allocPrint(allocator, "PRAGMA auto_vacuum = {s}\x00", .{mode});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Sets the cache size.
    pub fn setCacheSize(self: *Self, allocator: std.mem.Allocator, size: i32) !void {
        const sql = try std.fmt.allocPrint(allocator, "PRAGMA cache_size = {d}\x00", .{size});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Enables or disables case-sensitive LIKE.
    pub fn setCaseSensitiveLike(self: *Self, enabled: bool) Error!void {
        const sql: [:0]const u8 = if (enabled) "PRAGMA case_sensitive_like = ON" else "PRAGMA case_sensitive_like = OFF";
        try self.exec(sql);
    }

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

    /// Sets the locking mode.
    pub fn setLockingMode(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
        const sql = try std.fmt.allocPrint(allocator, "PRAGMA locking_mode = {s}\x00", .{mode});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

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

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

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

    /// Sets the page size.
    pub fn setPageSize(self: *Self, allocator: std.mem.Allocator, size: u32) !void {
        const sql = try std.fmt.allocPrint(allocator, "PRAGMA page_size = {d}\x00", .{size});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Sets the maximum page count.
    pub fn setMaxPageCount(self: *Self, allocator: std.mem.Allocator, count: u32) !void {
        const sql = try std.fmt.allocPrint(allocator, "PRAGMA max_page_count = {d}\x00", .{count});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Sets the temp_store location.
    pub fn setTempStore(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
        const sql = try std.fmt.allocPrint(allocator, "PRAGMA temp_store = {s}\x00", .{mode});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Sets the WAL auto-checkpoint interval.
    pub fn setWalAutoCheckpoint(self: *Self, allocator: std.mem.Allocator, pages: u32) !void {
        const sql = try std.fmt.allocPrint(allocator, "PRAGMA wal_autocheckpoint = {d}\x00", .{pages});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    // ========================================================================
    // Maintenance
    // ========================================================================

    /// Runs integrity check and returns result.
    pub fn integrityCheck(self: *Self, allocator: std.mem.Allocator) ![]u8 {
        var stmt = try self.prepare("PRAGMA integrity_check");
        defer stmt.finalize();

        if (try stmt.step()) {
            if (stmt.columnText(0)) |text| {
                return try allocator.dupe(u8, text);
            }
        }
        return try allocator.dupe(u8, "unknown");
    }

    /// Runs quick integrity check.
    pub fn quickCheck(self: *Self, allocator: std.mem.Allocator) ![]u8 {
        var stmt = try self.prepare("PRAGMA quick_check");
        defer stmt.finalize();

        if (try stmt.step()) {
            if (stmt.columnText(0)) |text| {
                return try allocator.dupe(u8, text);
            }
        }
        return try allocator.dupe(u8, "unknown");
    }

    /// Runs VACUUM to rebuild the database file.
    pub fn vacuum(self: *Self) Error!void {
        try self.exec("VACUUM");
    }

    /// Runs incremental vacuum.
    pub fn incrementalVacuum(self: *Self, allocator: std.mem.Allocator, pages: ?u32) !void {
        if (pages) |p| {
            const sql = try std.fmt.allocPrint(allocator, "PRAGMA incremental_vacuum({d})\x00", .{p});
            defer allocator.free(sql);
            try self.exec(sql[0 .. sql.len - 1 :0]);
        } else {
            try self.exec("PRAGMA incremental_vacuum");
        }
    }

    /// Performs a WAL checkpoint.
    pub fn walCheckpoint(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
        const sql = try std.fmt.allocPrint(allocator, "PRAGMA wal_checkpoint({s})\x00", .{mode});
        defer allocator.free(sql);
        try self.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Optimizes the database.
    pub fn optimize(self: *Self) Error!void {
        try self.exec("PRAGMA optimize");
    }

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

    /// Attaches another database file to this connection.
    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.
    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.
    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.
    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.
    pub fn createScalarFunction(self: *Self, name: [:0]const u8, num_args: i32, func: ScalarFn) !void {
        const wrapper = try ScalarFnWrapper.create(func);
        const result = c.sqlite3_create_function_v2(
            self.handle,
            name.ptr,
            num_args,
            c.SQLITE_UTF8,
            wrapper,
            functions.scalarCallback,
            null,
            null,
            functions.scalarDestructor,
        );

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

    /// Removes a function from the database.
    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 an aggregate function with the database.
    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,
            functions.aggregateStepCallback,
            functions.aggregateFinalCallback,
            functions.aggregateDestructor,
        );

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

    /// Registers a window function with the database.
    pub fn createWindowFunction(
        self: *Self,
        name: [:0]const u8,
        num_args: i32,
        step_fn: AggregateStepFn,
        final_fn: AggregateFinalFn,
        value_fn: WindowValueFn,
        inverse_fn: WindowInverseFn,
    ) !void {
        const wrapper = try WindowFnWrapper.create(step_fn, final_fn, value_fn, inverse_fn);
        const result = c.sqlite3_create_window_function(
            self.handle,
            name.ptr,
            num_args,
            c.SQLITE_UTF8,
            wrapper,
            functions.windowStepCallback,
            functions.windowFinalCallback,
            functions.windowValueCallback,
            functions.windowInverseCallback,
            functions.windowDestructor,
        );

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

    // ========================================================================
    // Collations
    // ========================================================================

    /// Registers a custom collation sequence.
    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,
            functions.collationCallback,
            functions.collationDestructor,
        );

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

    /// Removes a custom collation sequence.
    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.
    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, functions.commitHookCallback, wrapper);
            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.
    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, functions.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.
    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, functions.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();
            }
        }
    }

    /// Sets a pre-update hook callback.
    pub fn setPreUpdateHook(self: *Self, func: ?ZigPreUpdateHookFn) !void {
        if (func) |f| {
            const wrapper = try PreUpdateHookWrapper.create(f);
            const old = c.sqlite3_preupdate_hook(self.handle, functions.preUpdateHookCallback, wrapper);
            if (old != null) {
                const old_wrapper: *PreUpdateHookWrapper = @ptrCast(@alignCast(old));
                old_wrapper.destroy();
            }
        } else {
            const old = c.sqlite3_preupdate_hook(self.handle, null, null);
            if (old != null) {
                const old_wrapper: *PreUpdateHookWrapper = @ptrCast(@alignCast(old));
                old_wrapper.destroy();
            }
        }
    }

    /// Removes all hooks.
    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();
        }
    }

    // ========================================================================
    // Authorizer
    // ========================================================================

    /// Sets an authorizer callback.
    pub fn setAuthorizer(self: *Self, func: ?ZigAuthorizerFn) !void {
        if (func) |f| {
            const wrapper = try AuthorizerWrapper.create(f);
            const result = c.sqlite3_set_authorizer(self.handle, functions.authorizerCallback, wrapper);
            if (result != c.SQLITE_OK) {
                wrapper.destroy();
                return resultToError(result);
            }
        } else {
            const result = c.sqlite3_set_authorizer(self.handle, null, null);
            if (result != c.SQLITE_OK) {
                return resultToError(result);
            }
        }
    }

    // ========================================================================
    // Progress Handler
    // ========================================================================

    /// Sets a progress handler callback.
    pub fn setProgressHandler(self: *Self, n_ops: i32, func: ?ZigProgressFn) !void {
        if (func) |f| {
            const wrapper = try ProgressWrapper.create(f);
            c.sqlite3_progress_handler(self.handle, n_ops, functions.progressCallback, wrapper);
        } else {
            c.sqlite3_progress_handler(self.handle, 0, null, null);
        }
    }

    // ========================================================================
    // Busy Handler
    // ========================================================================

    /// Sets a custom busy handler callback.
    pub fn setBusyHandler(self: *Self, func: ?ZigBusyHandlerFn) !void {
        if (func) |f| {
            const wrapper = try BusyHandlerWrapper.create(f);
            const result = c.sqlite3_busy_handler(self.handle, functions.busyHandlerCallback, wrapper);
            if (result != c.SQLITE_OK) {
                wrapper.destroy();
                return resultToError(result);
            }
        } else {
            const result = c.sqlite3_busy_handler(self.handle, null, null);
            if (result != c.SQLITE_OK) {
                return resultToError(result);
            }
        }
    }

    // ========================================================================
    // Limits
    // ========================================================================

    /// Gets the current value of a limit.
    pub fn getLimit(self: *Self, limit_type: Limit) i32 {
        return c.sqlite3_limit(self.handle, @intFromEnum(limit_type), -1);
    }

    /// Sets a new value for a limit and returns the previous value.
    pub fn setLimit(self: *Self, limit_type: Limit, new_value: i32) i32 {
        return c.sqlite3_limit(self.handle, @intFromEnum(limit_type), new_value);
    }

    // ========================================================================
    // File Control
    // ========================================================================

    /// File control operation codes.
    pub const FileControlOp = enum(c_int) {
        lockstate = 1,
        get_lockproxyfile = 2,
        set_lockproxyfile = 3,
        last_errno = 4,
        size_hint = 5,
        chunk_size = 6,
        file_pointer = 7,
        sync_omitted = 8,
        win32_av_retry = 9,
        persist_wal = 10,
        overwrite = 11,
        vfsname = 12,
        powersafe_overwrite = 13,
        pragma = 14,
        busyhandler = 15,
        tempfilename = 16,
        mmap_size = 18,
        trace = 19,
        has_moved = 20,
        sync = 21,
        commit_phasetwo = 22,
        win32_set_handle = 23,
        wal_block = 24,
        zipvfs = 25,
        rbu = 26,
        vfs_pointer = 27,
        journal_pointer = 28,
        win32_get_handle = 29,
        pdb = 30,
        begin_atomic_write = 31,
        commit_atomic_write = 32,
        rollback_atomic_write = 33,
        lock_timeout = 34,
        data_version = 35,
        size_limit = 36,
        ckpt_done = 37,
        reserve_bytes = 38,
        ckpt_start = 39,
        external_reader = 40,
        cksm_file = 41,
        reset_cache = 42,
    };

    /// Performs a file control operation with an integer value.
    ///
    /// This is a low-level interface to the sqlite3_file_control() function.
    /// Most applications should use the higher-level pragma interface instead.
    ///
    /// Parameters:
    /// - db_name: The database name ("main", "temp", or attached db name)
    /// - op: The file control operation code
    /// - value: Pointer to the integer value (input/output depending on operation)
    ///
    /// Returns error on failure.
    pub fn setFileControlInt(self: *Self, db_name: [:0]const u8, op: FileControlOp, value: *i32) Error!void {
        const result = c.sqlite3_file_control(self.handle, db_name.ptr, @intFromEnum(op), value);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Gets persist_wal setting.
    pub fn getPersistWal(self: *Self, db_name: [:0]const u8) Error!bool {
        var value: i32 = -1;
        try self.setFileControlInt(db_name, .persist_wal, &value);
        return value != 0;
    }

    /// Sets persist_wal setting.
    pub fn setPersistWal(self: *Self, db_name: [:0]const u8, persist: bool) Error!void {
        var value: i32 = if (persist) 1 else 0;
        try self.setFileControlInt(db_name, .persist_wal, &value);
    }

    /// Gets powersafe_overwrite setting.
    pub fn getPowersafeOverwrite(self: *Self, db_name: [:0]const u8) Error!bool {
        var value: i32 = -1;
        try self.setFileControlInt(db_name, .powersafe_overwrite, &value);
        return value != 0;
    }

    /// Sets powersafe_overwrite setting.
    pub fn setPowersafeOverwrite(self: *Self, db_name: [:0]const u8, enabled: bool) Error!void {
        var value: i32 = if (enabled) 1 else 0;
        try self.setFileControlInt(db_name, .powersafe_overwrite, &value);
    }

    /// Sets the chunk size for file growth.
    pub fn setChunkSize(self: *Self, db_name: [:0]const u8, size: i32) Error!void {
        var value: i32 = size;
        try self.setFileControlInt(db_name, .chunk_size, &value);
    }

    /// Gets the data version (incremented each time the database is modified).
    pub fn getDataVersion(self: *Self, db_name: [:0]const u8) Error!u32 {
        var value: i32 = 0;
        try self.setFileControlInt(db_name, .data_version, &value);
        return @intCast(value);
    }

    // ========================================================================
    // VACUUM INTO
    // ========================================================================

    /// Creates a vacuumed copy of the database to a new file.
    ///
    /// VACUUM INTO creates a compacted, defragmented copy of the database
    /// in a new file. This is useful for:
    ///
    /// - Creating backups that are smaller and faster to restore
    /// - Defragmenting a database without modifying the original
    /// - Creating a snapshot while the database is in use
    ///
    /// The target file must not exist or be empty.
    ///
    /// Example:
    /// ```zig
    /// try db.vacuumInto("backup.sqlite");
    /// ```
    ///
    /// Note: Requires SQLite 3.27.0 or later.
    pub fn vacuumInto(self: *Self, path: []const u8) !void {
        const sql = try std.fmt.allocPrint(
            std.heap.c_allocator,
            "VACUUM INTO '{s}'",
            .{path},
        );
        defer std.heap.c_allocator.free(sql);

        const sql_z = try std.heap.c_allocator.dupeZ(u8, sql);
        defer std.heap.c_allocator.free(sql_z);

        try self.exec(sql_z);
    }

    /// Creates a vacuumed copy of a specific schema to a new file.
    ///
    /// Parameters:
    /// - schema: Database schema ("main", or attached database name)
    /// - path: Target file path
    ///
    /// Example:
    /// ```zig
    /// try db.vacuumSchemaInto("main", "backup.sqlite");
    /// try db.vacuumSchemaInto("attached_db", "attached_backup.sqlite");
    /// ```
    pub fn vacuumSchemaInto(self: *Self, schema: []const u8, path: []const u8) !void {
        const sql = try std.fmt.allocPrint(
            std.heap.c_allocator,
            "VACUUM {s} INTO '{s}'",
            .{ schema, path },
        );
        defer std.heap.c_allocator.free(sql);

        const sql_z = try std.heap.c_allocator.dupeZ(u8, sql);
        defer std.heap.c_allocator.free(sql_z);

        try self.exec(sql_z);
    }

    // ========================================================================
    // Snapshot API (for consistent reads)
    // ========================================================================

    /// A database snapshot handle.
    ///
    /// Snapshots allow reading from a consistent point in time, even while
    /// other connections are writing to the database. This is useful for:
    ///
    /// - Long-running read transactions that shouldn't block writers
    /// - Creating consistent backups
    /// - Time-travel queries (reading historical data)
    ///
    /// Note: Requires WAL mode and SQLite 3.21.0 or later.
    pub const Snapshot = struct {
        handle: ?*c.sqlite3_snapshot,

        const SnapshotSelf = @This();

        /// Frees the snapshot.
        pub fn deinit(snapshot_self: *SnapshotSelf) void {
            if (snapshot_self.handle) |h| {
                c.sqlite3_snapshot_free(h);
                snapshot_self.handle = null;
            }
        }

        /// Compares two snapshots.
        /// Returns < 0 if self is older, 0 if same, > 0 if self is newer.
        pub fn compare(snapshot_self: *const SnapshotSelf, other: *const SnapshotSelf) i32 {
            return c.sqlite3_snapshot_cmp(snapshot_self.handle, other.handle);
        }
    };

    /// Gets a snapshot of the current database state.
    ///
    /// The database must be in WAL mode and have an active read transaction.
    /// Call this after BEGIN and before the first read to capture the snapshot.
    ///
    /// Example:
    /// ```zig
    /// try db.exec("PRAGMA journal_mode=WAL");
    /// try db.begin();
    /// const snapshot = try db.getSnapshot("main");
    /// defer snapshot.deinit();
    /// // Use snapshot.handle with openSnapshot on another connection
    /// ```
    pub fn getSnapshot(self: *Self, schema: [:0]const u8) Error!Snapshot {
        var handle: ?*c.sqlite3_snapshot = null;

        const result = c.sqlite3_snapshot_get(self.handle, schema.ptr, &handle);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }

        return Snapshot{ .handle = handle };
    }

    /// Opens the database at a specific snapshot.
    ///
    /// This allows reading data as it existed at the time the snapshot
    /// was taken, even if the database has been modified since.
    ///
    /// The connection must be in WAL mode with an active read transaction.
    ///
    /// Example:
    /// ```zig
    /// var reader = try Database.open("mydb.sqlite");
    /// try reader.exec("PRAGMA journal_mode=WAL");
    /// try reader.begin();
    /// try reader.openSnapshot("main", snapshot);
    /// // Now reader sees data as of snapshot time
    /// ```
    pub fn openSnapshot(self: *Self, schema: [:0]const u8, snapshot: *const Snapshot) Error!void {
        const result = c.sqlite3_snapshot_open(self.handle, schema.ptr, snapshot.handle);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Attempts to recover a snapshot.
    ///
    /// If the snapshot is still available (WAL hasn't been checkpointed past it),
    /// this reopens the connection at that snapshot. Otherwise returns error.
    pub fn recoverSnapshot(self: *Self, schema: [:0]const u8) Error!void {
        const result = c.sqlite3_snapshot_recover(self.handle, schema.ptr);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }
};
