//! FTS5 Full-Text Search Helpers
//!
//! Provides convenient wrappers for SQLite's FTS5 full-text search extension.
//! FTS5 is compiled into SQLite by default with -DSQLITE_ENABLE_FTS5.

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

/// FTS5 tokenizer options.
pub const TokenizerOption = enum {
    unicode61,
    ascii,
    porter,
    trigram,

    pub fn name(self: TokenizerOption) []const u8 {
        return switch (self) {
            .unicode61 => "unicode61",
            .ascii => "ascii",
            .porter => "porter",
            .trigram => "trigram",
        };
    }
};

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

    const Self = @This();

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

    /// Creates a simple FTS5 table with specified columns.
    pub fn createSimpleTable(self: *Self, table_name: []const u8, columns: []const []const u8) !void {
        try self.createTableWithTokenizer(table_name, columns, .unicode61);
    }

    /// Creates an FTS5 table with a specific tokenizer.
    pub fn createTableWithTokenizer(
        self: *Self,
        table_name: []const u8,
        columns: []const []const u8,
        tokenizer: TokenizerOption,
    ) !void {
        // Build column list
        var col_buf: [4096]u8 = undefined;
        var col_len: usize = 0;

        for (columns, 0..) |col, i| {
            if (i > 0) {
                col_buf[col_len] = ',';
                col_buf[col_len + 1] = ' ';
                col_len += 2;
            }
            @memcpy(col_buf[col_len..][0..col.len], col);
            col_len += col.len;
        }

        const sql = try std.fmt.allocPrint(
            self.allocator,
            "CREATE VIRTUAL TABLE {s} USING fts5({s}, tokenize='{s}')\x00",
            .{ table_name, col_buf[0..col_len], tokenizer.name() },
        );
        defer self.allocator.free(sql);
        try self.db.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Drops an FTS5 table.
    pub fn dropTable(self: *Self, table_name: []const u8) !void {
        const sql = try std.fmt.allocPrint(self.allocator, "DROP TABLE IF EXISTS {s}\x00", .{table_name});
        defer self.allocator.free(sql);
        try self.db.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Performs a full-text search query.
    pub fn search(
        self: *Self,
        table_name: []const u8,
        query: []const u8,
        result_columns: []const []const u8,
        limit: ?u32,
    ) !Statement {
        // Build column list
        var col_buf: [4096]u8 = undefined;
        var col_len: usize = 0;

        if (result_columns.len == 0) {
            col_buf[0] = '*';
            col_len = 1;
        } else {
            for (result_columns, 0..) |col, i| {
                if (i > 0) {
                    col_buf[col_len] = ',';
                    col_buf[col_len + 1] = ' ';
                    col_len += 2;
                }
                @memcpy(col_buf[col_len..][0..col.len], col);
                col_len += col.len;
            }
        }

        const sql = if (limit) |l|
            try std.fmt.allocPrint(
                self.allocator,
                "SELECT {s} FROM {s} WHERE {s} MATCH ? ORDER BY rank LIMIT {d}\x00",
                .{ col_buf[0..col_len], table_name, table_name, l },
            )
        else
            try std.fmt.allocPrint(
                self.allocator,
                "SELECT {s} FROM {s} WHERE {s} MATCH ? ORDER BY rank\x00",
                .{ col_buf[0..col_len], table_name, table_name },
            );
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        try stmt.bindText(1, query);
        return stmt;
    }

    /// Gets highlighted snippet from search results.
    pub fn searchWithHighlight(
        self: *Self,
        table_name: []const u8,
        query: []const u8,
        column_index: u32,
        before_match: []const u8,
        after_match: []const u8,
        limit: ?u32,
    ) !Statement {
        const sql = if (limit) |l|
            try std.fmt.allocPrint(
                self.allocator,
                "SELECT highlight({s}, {d}, '{s}', '{s}') FROM {s} WHERE {s} MATCH ? ORDER BY rank LIMIT {d}\x00",
                .{ table_name, column_index, before_match, after_match, table_name, table_name, l },
            )
        else
            try std.fmt.allocPrint(
                self.allocator,
                "SELECT highlight({s}, {d}, '{s}', '{s}') FROM {s} WHERE {s} MATCH ? ORDER BY rank\x00",
                .{ table_name, column_index, before_match, after_match, table_name, table_name },
            );
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        try stmt.bindText(1, query);
        return stmt;
    }

    /// Gets a snippet (context around matches) from search results.
    pub fn searchWithSnippet(
        self: *Self,
        table_name: []const u8,
        query: []const u8,
        column_index: i32,
        before_match: []const u8,
        after_match: []const u8,
        ellipsis: []const u8,
        max_tokens: u32,
        limit: ?u32,
    ) !Statement {
        const sql = if (limit) |l|
            try std.fmt.allocPrint(
                self.allocator,
                "SELECT snippet({s}, {d}, '{s}', '{s}', '{s}', {d}) FROM {s} WHERE {s} MATCH ? ORDER BY rank LIMIT {d}\x00",
                .{ table_name, column_index, before_match, after_match, ellipsis, max_tokens, table_name, table_name, l },
            )
        else
            try std.fmt.allocPrint(
                self.allocator,
                "SELECT snippet({s}, {d}, '{s}', '{s}', '{s}', {d}) FROM {s} WHERE {s} MATCH ? ORDER BY rank\x00",
                .{ table_name, column_index, before_match, after_match, ellipsis, max_tokens, table_name, table_name },
            );
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        try stmt.bindText(1, query);
        return stmt;
    }

    /// Rebuilds the FTS5 index.
    pub fn rebuild(self: *Self, table_name: []const u8) !void {
        const sql = try std.fmt.allocPrint(self.allocator, "INSERT INTO {s}({s}) VALUES('rebuild')\x00", .{ table_name, table_name });
        defer self.allocator.free(sql);
        try self.db.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Optimizes the FTS5 index by merging segments.
    pub fn optimize(self: *Self, table_name: []const u8) !void {
        const sql = try std.fmt.allocPrint(self.allocator, "INSERT INTO {s}({s}) VALUES('optimize')\x00", .{ table_name, table_name });
        defer self.allocator.free(sql);
        try self.db.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Runs integrity check on the FTS5 index.
    pub fn integrityCheck(self: *Self, table_name: []const u8) !void {
        const sql = try std.fmt.allocPrint(self.allocator, "INSERT INTO {s}({s}) VALUES('integrity-check')\x00", .{ table_name, table_name });
        defer self.allocator.free(sql);
        try self.db.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Deletes all content from the FTS5 table.
    pub fn deleteAll(self: *Self, table_name: []const u8) !void {
        const sql = try std.fmt.allocPrint(self.allocator, "DELETE FROM {s}\x00", .{table_name});
        defer self.allocator.free(sql);
        try self.db.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Gets the total number of rows in the FTS5 table.
    pub fn count(self: *Self, table_name: []const u8) !i64 {
        const sql = try std.fmt.allocPrint(self.allocator, "SELECT COUNT(*) FROM {s}\x00", .{table_name});
        defer self.allocator.free(sql);

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

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