//! SQLite Backup and Blob I/O
//!
//! Provides the Backup struct for database copying and the Blob struct
//! for incremental blob I/O operations.

const std = @import("std");
const c = @import("c.zig").c;
const errors = @import("errors.zig");

const Error = errors.Error;
const resultToError = errors.resultToError;

pub const Database = @import("database.zig").Database;

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

/// SQLite online backup handle.
///
/// Allows copying database content from one database to another while
/// both databases are in use.
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.
    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) {
            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.
    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.
    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.
    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 {};
    }
};

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

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

/// Copies an entire database to another database.
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.
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.
pub fn loadFromFile(path: [:0]const u8) Error!Database {
    var file_db = try Database.open(path);
    defer file_db.close();

    var mem_db = try Database.open(":memory:");
    errdefer mem_db.close();

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