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

// Helper function to clean highlight text by removing page/margin reference numbers
// Removes patterns like "10After" -> "After" and " 5good" -> " good"
fn cleanHighlightText(allocator: std.mem.Allocator, text: []const u8) ![]const u8 {
    var result = std.ArrayList(u8).initCapacity(allocator, 0) catch unreachable;
    defer result.deinit(allocator);
    const writer = result.writer(allocator);

    var i: usize = 0;
    while (i < text.len) {
        // Check if we're at start of text or after whitespace/newline
        const at_boundary = (i == 0) or (text[i - 1] == ' ') or (text[i - 1] == '\n') or (text[i - 1] == '\t');

        if (at_boundary and std.ascii.isDigit(text[i])) {
            // Count consecutive digits
            var digit_end = i;
            while (digit_end < text.len and std.ascii.isDigit(text[digit_end])) {
                digit_end += 1;
            }

            // If digits are followed by a letter (like "10After"), skip the digits
            if (digit_end < text.len and std.ascii.isAlphabetic(text[digit_end])) {
                i = digit_end; // Skip the digits
                continue;
            }
        }

        try writer.writeByte(text[i]);
        i += 1;
    }

    return try allocator.dupe(u8, result.items);
}

// Helper function to escape JSON strings
fn escapeJson(allocator: std.mem.Allocator, s: []const u8) ![]const u8 {
    var result = std.ArrayList(u8).initCapacity(allocator, 0) catch unreachable;
    defer result.deinit(allocator);
    const writer = result.writer(allocator);

    try writer.writeAll("\"");
    for (s) |ch| {
        switch (ch) {
            '"' => try writer.writeAll("\\\""),
            '\\' => try writer.writeAll("\\\\"),
            '\n' => try writer.writeAll("\\n"),
            '\r' => try writer.writeAll("\\r"),
            '\t' => try writer.writeAll("\\t"),
            else => try writer.writeByte(ch),
        }
    }
    try writer.writeAll("\"");

    return try allocator.dupe(u8, result.items);
}

const Highlight = struct {
    text: []const u8, // Owned copy
    date: []const u8, // Owned copy
    book: []const u8, // Owned copy
    text_hash: [64]u8, // Stack allocated - no cleanup needed
    book_hash: [64]u8, // Stack allocated - no cleanup needed
    allocator: std.mem.Allocator,

    // JSON-serializable version (no allocator field)
    const JsonHighlight = struct {
        text: []const u8,
        date: []const u8,
        book: []const u8,
        text_hash: []const u8, // Slice view of the hash array
        book_hash: []const u8, // Slice view of the hash array
    };

    fn toJson(self: *const Highlight) JsonHighlight {
        return JsonHighlight{
            .text = self.text,
            .date = self.date,
            .book = self.book,
            .text_hash = &self.text_hash,
            .book_hash = &self.book_hash,
        };
    }

    fn init(allocator: std.mem.Allocator, text: []const u8, date: []const u8, book: []const u8, content_id_raw: []const u8) !Highlight {
        // Clean the text to remove page reference numbers
        const cleaned_text = try cleanHighlightText(allocator, text);
        defer allocator.free(cleaned_text);

        var highlight = Highlight{
            .text = try allocator.dupe(u8, cleaned_text), // Copy cleaned text
            .date = try allocator.dupe(u8, date), // Copy from SQLite buffer
            .book = try allocator.dupe(u8, book), // Copy from SQLite buffer
            .text_hash = undefined,
            .book_hash = undefined,
            .allocator = allocator,
        };

        // Now compute hashes into the struct fields (use cleaned text for hash)
        computeHash(cleaned_text, &highlight.text_hash);
        computeHash(content_id_raw, &highlight.book_hash);

        return highlight;
    }

    fn deinit(self: *Highlight) void {
        self.allocator.free(self.text);
        self.allocator.free(self.date);
        self.allocator.free(self.book);
    }

    fn computeHash(data: []const u8, out_hex: *[64]u8) void {
        var hash: [32]u8 = undefined; // SHA256 produces 32 bytes
        std.crypto.hash.sha2.Sha256.hash(data, &hash, .{});

        // Convert 32 bytes to 64 hex characters
        const hex_chars = "0123456789abcdef";
        for (hash, 0..) |byte, i| {
            out_hex[i * 2] = hex_chars[byte >> 4];
            out_hex[i * 2 + 1] = hex_chars[byte & 0x0F];
        }
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Get command line args
    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    if (args.len < 2) {
        std.debug.print("Usage: {s} <path-to-kobo-database.sqlite> [flask-endpoint-url]\n", .{args[0]});
        std.debug.print("Example: {s} /mnt/kobo/.kobo/KoboReader.sqlite\n", .{args[0]});
        std.debug.print("Example: {s} /mnt/kobo/.kobo/KoboReader.sqlite https://myserver.com/api/highlights\n", .{args[0]});
        return error.MissingArgument;
    }

    const db_path = args[1];
    const endpoint_url = if (args.len >= 3) args[2] else null;

    // Open database
    var db: ?*c.sqlite3 = null;
    const rc = c.sqlite3_open(db_path.ptr, &db);
    if (rc != c.SQLITE_OK) {
        std.debug.print("Failed to open database: {s}\n", .{c.sqlite3_errmsg(db)});
        return error.DatabaseOpenFailed;
    }
    defer _ = c.sqlite3_close(db);

    // Prepare query to get highlights
    const query =
        \\SELECT
        \\  Text,
        \\  DateCreated,
        \\  SUBSTR(
        \\    SUBSTR(ContentID, 40, INSTR(ContentID, '.epub') - 40 + 5),
        \\    INSTR(SUBSTR(ContentID, 40, INSTR(ContentID, '.epub') - 40 + 5), '/') + 1
        \\  ) AS Book,
        \\  ContentID
        \\FROM Bookmark
        \\WHERE Text IS NOT NULL
        \\ORDER BY DateCreated DESC;
    ;

    var stmt: ?*c.sqlite3_stmt = null;
    if (c.sqlite3_prepare_v2(db, query.ptr, @intCast(query.len), &stmt, null) != c.SQLITE_OK) {
        std.debug.print("Failed to prepare statement: {s}\n", .{c.sqlite3_errmsg(db)});
        return error.QueryPrepareFailed;
    }
    defer _ = c.sqlite3_finalize(stmt);

    // Create ArrayList to collect highlights
    var highlights = std.ArrayList(Highlight).initCapacity(allocator, 0) catch unreachable;
    defer {
        for (highlights.items) |*highlight| {
            highlight.deinit();
        }
        highlights.deinit(allocator);
    }

    // Execute query and collect results
    while (c.sqlite3_step(stmt) == c.SQLITE_ROW) {

        // Get the text (highlight) - convert C pointer to Zig slice
        const text_ptr = c.sqlite3_column_text(stmt, 0);
        const text = if (text_ptr != null)
            std.mem.span(@as([*:0]const u8, @ptrCast(text_ptr)))
        else
            "";

        // Get date - convert C pointer to Zig slice
        const date_ptr = c.sqlite3_column_text(stmt, 1);
        const date = if (date_ptr != null)
            std.mem.span(@as([*:0]const u8, @ptrCast(date_ptr)))
        else
            "unknown";

        // Get book title - convert C pointer to Zig slice
        const book_ptr = c.sqlite3_column_text(stmt, 2);
        const book = if (book_ptr != null)
            std.mem.span(@as([*:0]const u8, @ptrCast(book_ptr)))
        else
            "unknown";

        // Get raw ContentID for hash - convert C pointer to Zig slice
        const content_id_ptr = c.sqlite3_column_text(stmt, 3);
        const content_id = if (content_id_ptr != null)
            std.mem.span(@as([*:0]const u8, @ptrCast(content_id_ptr)))
        else
            "";

        // Create Highlight struct (makes owned copies and computes hashes)
        const highlight = try Highlight.init(allocator, text, date, book, content_id);

        // Add to ArrayList
        try highlights.append(allocator, highlight);
    }

    std.debug.print("Extracted {d} highlights from database\n", .{highlights.items.len});

    // Build JSON manually
    var json_buffer = std.ArrayList(u8).initCapacity(allocator, 0) catch unreachable;
    defer json_buffer.deinit(allocator);
    const writer = json_buffer.writer(allocator);

    try writer.writeAll("[");
    for (highlights.items, 0..) |*highlight, i| {
        if (i > 0) try writer.writeAll(",");
        try writer.writeAll("{");

        const escaped_text = try escapeJson(allocator, highlight.text);
        defer allocator.free(escaped_text);
        const escaped_book = try escapeJson(allocator, highlight.book);
        defer allocator.free(escaped_book);

        try writer.print("\"text\":{s},", .{escaped_text});
        try writer.print("\"date\":\"{s}\",", .{highlight.date});
        try writer.print("\"book\":{s},", .{escaped_book});
        try writer.print("\"text_hash\":\"{s}\",", .{highlight.text_hash});
        try writer.print("\"book_hash\":\"{s}\"", .{highlight.book_hash});
        try writer.writeAll("}");
    }
    try writer.writeAll("]");

    // If endpoint URL provided, POST the JSON; otherwise just print it
    if (endpoint_url) |url| {
        std.debug.print("Sending {d} highlights ({d} bytes) to {s}...\n", .{highlights.items.len, json_buffer.items.len, url});

        var client: std.http.Client = .{ .allocator = allocator };
        defer client.deinit();

        const uri = try std.Uri.parse(url);

        const result = try client.fetch(.{
            .location = .{ .uri = uri },
            .method = .POST,
            .payload = json_buffer.items,
            .headers = .{
                .content_type = .{ .override = "application/json" },
            },
        });

        const status_code = @intFromEnum(result.status);
        if (status_code >= 200 and status_code < 300) {
            std.debug.print("✓ Successfully sent highlights to server (HTTP {})\n", .{status_code});
        } else {
            std.debug.print("✗ Server returned error: HTTP {}\n", .{status_code});
            return error.HttpRequestFailed;
        }
    } else {
        std.debug.print("No endpoint provided. JSON payload ({d} bytes):\n{s}\n", .{json_buffer.items.len, json_buffer.items});
    }
}
