//! JSON1 Extension Helpers
//!
//! Provides convenient wrappers for SQLite's JSON1 extension functions.
//! JSON1 is compiled into SQLite by default with -DSQLITE_ENABLE_JSON1.

const std = @import("std");
const Database = @import("database.zig").Database;
const Statement = @import("statement.zig").Statement;
const Error = @import("errors.zig").Error;

/// JSON helper functions for a database connection.
pub const Json = struct {
    db: *Database,
    allocator: std.mem.Allocator,

    const Self = @This();

    /// Creates a new JSON helper bound to a database connection.
    pub fn init(db: *Database, allocator: std.mem.Allocator) Self {
        return .{ .db = db, .allocator = allocator };
    }

    // ========================================================================
    // JSON Validation and Parsing
    // ========================================================================

    /// Validates and minifies a JSON string.
    pub fn validate(self: *Self, json_text: []const u8) !?[]u8 {
        var stmt = try self.db.prepare("SELECT json(?)");
        defer stmt.finalize();

        try stmt.bindText(1, json_text);

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

    /// Checks if a string is valid JSON.
    pub fn isValid(self: *Self, json_text: []const u8) !bool {
        var stmt = try self.db.prepare("SELECT json_valid(?)");
        defer stmt.finalize();

        try stmt.bindText(1, json_text);

        if (try stmt.step()) {
            return stmt.columnInt(0) == 1;
        }
        return false;
    }

    /// Returns the JSON type of a value at a path.
    pub fn getType(self: *Self, json_text: []const u8, path: []const u8) !?[]u8 {
        var stmt = try self.db.prepare("SELECT json_type(?, ?)");
        defer stmt.finalize();

        try stmt.bindText(1, json_text);
        try stmt.bindText(2, path);

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

    // ========================================================================
    // JSON Extraction
    // ========================================================================

    /// Extracts a value from JSON at the given path.
    pub fn extract(self: *Self, json_text: []const u8, path: []const u8) !?[]u8 {
        var stmt = try self.db.prepare("SELECT json_extract(?, ?)");
        defer stmt.finalize();

        try stmt.bindText(1, json_text);
        try stmt.bindText(2, path);

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

    /// Extracts an integer from JSON at the given path.
    pub fn extractInt(self: *Self, json_text: []const u8, path: []const u8) !?i64 {
        var stmt = try self.db.prepare("SELECT json_extract(?, ?)");
        defer stmt.finalize();

        try stmt.bindText(1, json_text);
        try stmt.bindText(2, path);

        if (try stmt.step()) {
            if (!stmt.columnIsNull(0)) {
                return stmt.columnInt(0);
            }
        }
        return null;
    }

    /// Extracts a float from JSON at the given path.
    pub fn extractFloat(self: *Self, json_text: []const u8, path: []const u8) !?f64 {
        var stmt = try self.db.prepare("SELECT json_extract(?, ?)");
        defer stmt.finalize();

        try stmt.bindText(1, json_text);
        try stmt.bindText(2, path);

        if (try stmt.step()) {
            if (!stmt.columnIsNull(0)) {
                return stmt.columnFloat(0);
            }
        }
        return null;
    }

    /// Extracts a boolean from JSON at the given path.
    pub fn extractBool(self: *Self, json_text: []const u8, path: []const u8) !?bool {
        const result = try self.extractInt(json_text, path);
        if (result) |v| {
            return v != 0;
        }
        return null;
    }

    // ========================================================================
    // JSON Modification
    // ========================================================================

    /// Inserts a value into JSON at the given path (only if path doesn't exist).
    pub fn insert(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
        var stmt = try self.db.prepare("SELECT json_insert(?, ?, json(?))");
        defer stmt.finalize();

        try stmt.bindText(1, json_text);
        try stmt.bindText(2, path);
        try stmt.bindText(3, value);

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

    /// Replaces a value in JSON at the given path (only if path exists).
    pub fn replace(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
        var stmt = try self.db.prepare("SELECT json_replace(?, ?, json(?))");
        defer stmt.finalize();

        try stmt.bindText(1, json_text);
        try stmt.bindText(2, path);
        try stmt.bindText(3, value);

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

    /// Sets a value in JSON at the given path (insert or replace).
    pub fn set(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
        var stmt = try self.db.prepare("SELECT json_set(?, ?, json(?))");
        defer stmt.finalize();

        try stmt.bindText(1, json_text);
        try stmt.bindText(2, path);
        try stmt.bindText(3, value);

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

    /// Sets a string value in JSON at the given path.
    pub fn setString(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
        var stmt = try self.db.prepare("SELECT json_set(?, ?, ?)");
        defer stmt.finalize();

        try stmt.bindText(1, json_text);
        try stmt.bindText(2, path);
        try stmt.bindText(3, value);

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

    /// Sets an integer value in JSON at the given path.
    pub fn setInt(self: *Self, json_text: []const u8, path: []const u8, value: i64) ![]u8 {
        var stmt = try self.db.prepare("SELECT json_set(?, ?, ?)");
        defer stmt.finalize();

        try stmt.bindText(1, json_text);
        try stmt.bindText(2, path);
        try stmt.bindInt(3, value);

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

    /// Sets a float value in JSON at the given path.
    pub fn setFloat(self: *Self, json_text: []const u8, path: []const u8, value: f64) ![]u8 {
        var stmt = try self.db.prepare("SELECT json_set(?, ?, ?)");
        defer stmt.finalize();

        try stmt.bindText(1, json_text);
        try stmt.bindText(2, path);
        try stmt.bindFloat(3, value);

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

    /// Sets a boolean value in JSON at the given path.
    pub fn setBool(self: *Self, json_text: []const u8, path: []const u8, value: bool) ![]u8 {
        const json_bool = if (value) "true" else "false";
        var stmt = try self.db.prepare("SELECT json_set(?, ?, json(?))");
        defer stmt.finalize();

        try stmt.bindText(1, json_text);
        try stmt.bindText(2, path);
        try stmt.bindText(3, json_bool);

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

    /// Removes a value from JSON at the given path.
    pub fn remove(self: *Self, json_text: []const u8, path: []const u8) ![]u8 {
        var stmt = try self.db.prepare("SELECT json_remove(?, ?)");
        defer stmt.finalize();

        try stmt.bindText(1, json_text);
        try stmt.bindText(2, path);

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

    // ========================================================================
    // JSON Array Operations
    // ========================================================================

    /// Returns the length of a JSON array.
    pub fn arrayLength(self: *Self, json_text: []const u8, path: ?[]const u8) !?i64 {
        if (path) |p| {
            var stmt = try self.db.prepare("SELECT json_array_length(?, ?)");
            defer stmt.finalize();
            try stmt.bindText(1, json_text);
            try stmt.bindText(2, p);
            if (try stmt.step()) {
                if (!stmt.columnIsNull(0)) {
                    return stmt.columnInt(0);
                }
            }
        } else {
            var stmt = try self.db.prepare("SELECT json_array_length(?)");
            defer stmt.finalize();
            try stmt.bindText(1, json_text);
            if (try stmt.step()) {
                if (!stmt.columnIsNull(0)) {
                    return stmt.columnInt(0);
                }
            }
        }
        return null;
    }

    /// Creates a JSON array from string values.
    pub fn createArray(self: *Self, values: []const []const u8) ![]u8 {
        if (values.len == 0) {
            return try self.allocator.dupe(u8, "[]");
        }

        // Build placeholders
        var placeholders: [256]u8 = undefined;
        var ph_len: usize = 0;
        for (0..values.len) |i| {
            if (i > 0) {
                placeholders[ph_len] = ',';
                placeholders[ph_len + 1] = ' ';
                ph_len += 2;
            }
            placeholders[ph_len] = '?';
            ph_len += 1;
        }

        const sql = try std.fmt.allocPrint(self.allocator, "SELECT json_array({s})\x00", .{placeholders[0..ph_len]});
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        defer stmt.finalize();

        for (values, 0..) |v, i| {
            try stmt.bindText(@intCast(i + 1), v);
        }

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

    /// Creates a JSON object from key-value pairs.
    pub fn createObject(self: *Self, keys: []const []const u8, values: []const []const u8) ![]u8 {
        if (keys.len != values.len) {
            return error.OutOfMemory; // Use a generic error
        }

        if (keys.len == 0) {
            return try self.allocator.dupe(u8, "{}");
        }

        // Build placeholders
        var placeholders: [512]u8 = undefined;
        var ph_len: usize = 0;
        for (0..keys.len) |i| {
            if (i > 0) {
                placeholders[ph_len] = ',';
                placeholders[ph_len + 1] = ' ';
                ph_len += 2;
            }
            placeholders[ph_len] = '?';
            placeholders[ph_len + 1] = ',';
            placeholders[ph_len + 2] = ' ';
            placeholders[ph_len + 3] = '?';
            ph_len += 4;
        }

        const sql = try std.fmt.allocPrint(self.allocator, "SELECT json_object({s})\x00", .{placeholders[0..ph_len]});
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        defer stmt.finalize();

        var param_idx: u32 = 1;
        for (keys, values) |k, v| {
            try stmt.bindText(param_idx, k);
            param_idx += 1;
            try stmt.bindText(param_idx, v);
            param_idx += 1;
        }

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

    /// Applies a JSON patch (RFC 7396) to a JSON document.
    pub fn patch(self: *Self, target: []const u8, patch_doc: []const u8) ![]u8 {
        var stmt = try self.db.prepare("SELECT json_patch(?, ?)");
        defer stmt.finalize();

        try stmt.bindText(1, target);
        try stmt.bindText(2, patch_doc);

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

    /// Returns a statement for iterating over JSON elements with json_each.
    pub fn each(self: *Self, json_text: []const u8, path: ?[]const u8) !Statement {
        if (path) |p| {
            var stmt = try self.db.prepare(
                "SELECT key, value, type, atom, id, parent, fullkey, path FROM json_each(?, ?)",
            );
            try stmt.bindText(1, json_text);
            try stmt.bindText(2, p);
            return stmt;
        } else {
            var stmt = try self.db.prepare(
                "SELECT key, value, type, atom, id, parent, fullkey, path FROM json_each(?)",
            );
            try stmt.bindText(1, json_text);
            return stmt;
        }
    }

    /// Returns a statement for iterating over all JSON elements with json_tree.
    pub fn tree(self: *Self, json_text: []const u8, path: ?[]const u8) !Statement {
        if (path) |p| {
            var stmt = try self.db.prepare(
                "SELECT key, value, type, atom, id, parent, fullkey, path FROM json_tree(?, ?)",
            );
            try stmt.bindText(1, json_text);
            try stmt.bindText(2, p);
            return stmt;
        } else {
            var stmt = try self.db.prepare(
                "SELECT key, value, type, atom, id, parent, fullkey, path FROM json_tree(?)",
            );
            try stmt.bindText(1, json_text);
            return stmt;
        }
    }
};
