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

/// Native hash context for high-performance file hashing
pub const NativeHasher = struct {
    ctx: *c.fh_context_t,
    allocator: std.mem.Allocator,

    /// Initialize native hasher with thread pool
    /// num_threads: 0 = auto-detect (use hardware concurrency)
    pub fn init(allocator: std.mem.Allocator, num_threads: u32) !NativeHasher {
        const ctx = c.fh_init(num_threads);
        if (ctx == null) return error.NativeInitFailed;

        return .{
            .ctx = ctx,
            .allocator = allocator,
        };
    }

    /// Cleanup native hasher and thread pool
    pub fn deinit(self: *NativeHasher) void {
        c.fh_cleanup(self.ctx);
    }

    /// Hash a single file
    pub fn hashFile(self: *NativeHasher, path: []const u8) ![]const u8 {
        const c_path = try self.allocator.dupeZ(u8, path);
        defer self.allocator.free(c_path);

        const result = c.fh_hash_file(self.ctx, c_path.ptr);
        if (result == null) return error.HashFailed;
        defer c.fh_free_string(result);

        return try self.allocator.dupe(u8, std.mem.span(result));
    }

    /// Batch hash multiple files (amortizes CGo overhead)
    pub fn hashBatch(self: *NativeHasher, paths: []const []const u8) ![][]const u8 {
        // Convert paths to C string array
        const c_paths = try self.allocator.alloc([*c]const u8, paths.len);
        defer self.allocator.free(c_paths);

        for (paths, 0..) |path, i| {
            const c_path = try self.allocator.dupeZ(u8, path);
            c_paths[i] = c_path.ptr;
            // Note: we need to keep these alive until after fh_hash_batch
        }
        defer {
            for (c_paths) |p| {
                self.allocator.free(std.mem.span(p));
            }
        }

        // Allocate results array
        const results = try self.allocator.alloc([*c]u8, paths.len);
        defer self.allocator.free(results);

        // Call native batch hash
        const ret = c.fh_hash_batch(self.ctx, c_paths.ptr, @intCast(paths.len), results.ptr);
        if (ret != 0) return error.HashFailed;

        // Convert results to Zig strings
        var hashes = try self.allocator.alloc([]const u8, paths.len);
        errdefer {
            for (hashes) |h| self.allocator.free(h);
            self.allocator.free(hashes);
        }

        for (results, 0..) |r, i| {
            hashes[i] = try self.allocator.dupe(u8, std.mem.span(r));
            c.fh_free_string(r);
        }

        return hashes;
    }

    /// Hash entire directory (combined hash)
    pub fn hashDirectory(self: *NativeHasher, dir_path: []const u8) ![]const u8 {
        const c_path = try self.allocator.dupeZ(u8, dir_path);
        defer self.allocator.free(c_path);

        const result = c.fh_hash_directory(self.ctx, c_path.ptr);
        if (result == null) return error.HashFailed;
        defer c.fh_free_string(result);

        return try self.allocator.dupe(u8, std.mem.span(result));
    }

    /// Hash directory with batch output (individual file hashes)
    pub fn hashDirectoryBatch(
        self: *NativeHasher,
        dir_path: []const u8,
        max_results: u32,
    ) !struct { hashes: [][]const u8, paths: [][]const u8, count: u32 } {
        const c_path = try self.allocator.dupeZ(u8, dir_path);
        defer self.allocator.free(c_path);

        // Allocate output arrays
        const hashes = try self.allocator.alloc([*c]u8, max_results);
        defer self.allocator.free(hashes);

        const paths = try self.allocator.alloc([*c]u8, max_results);
        defer self.allocator.free(paths);

        var count: u32 = 0;

        const ret = c.fh_hash_directory_batch(
            self.ctx,
            c_path.ptr,
            hashes.ptr,
            paths.ptr,
            max_results,
            &count,
        );
        if (ret != 0) return error.HashFailed;

        // Convert to Zig arrays
        var zig_hashes = try self.allocator.alloc([]const u8, count);
        errdefer {
            for (zig_hashes) |h| self.allocator.free(h);
            self.allocator.free(zig_hashes);
        }

        var zig_paths = try self.allocator.alloc([]const u8, count);
        errdefer {
            for (zig_paths) |p| self.allocator.free(p);
            self.allocator.free(zig_paths);
        }

        for (0..count) |i| {
            zig_hashes[i] = try self.allocator.dupe(u8, std.mem.span(hashes[i]));
            c.fh_free_string(hashes[i]);

            zig_paths[i] = try self.allocator.dupe(u8, std.mem.span(paths[i]));
            c.fh_free_string(paths[i]);
        }

        return .{
            .hashes = zig_hashes,
            .paths = zig_paths,
            .count = count,
        };
    }

    /// Check if SIMD SHA-256 is available
    pub fn hasSimd(self: *NativeHasher) bool {
        _ = self;
        return c.fh_has_simd_sha256() != 0;
    }

    /// Get implementation info (SIMD type, etc.)
    pub fn getImplInfo(self: *NativeHasher) []const u8 {
        _ = self;
        return std.mem.span(c.fh_get_simd_impl_name());
    }
};

/// Convenience function: hash directory using native library
pub fn hashDirectoryNative(allocator: std.mem.Allocator, dir_path: []const u8) ![]const u8 {
    var hasher = try NativeHasher.init(allocator, 0); // Auto-detect threads
    defer hasher.deinit();
    return try hasher.hashDirectory(dir_path);
}

/// Convenience function: batch hash files using native library
pub fn hashFilesNative(
    allocator: std.mem.Allocator,
    paths: []const []const u8,
) ![][]const u8 {
    var hasher = try NativeHasher.init(allocator, 0);
    defer hasher.deinit();
    return try hasher.hashBatch(paths);
}

test "NativeHasher basic operations" {
    const allocator = std.testing.allocator;

    // Skip if native library not available
    var hasher = NativeHasher.init(allocator, 1) catch |err| {
        if (err == error.NativeInitFailed) {
            std.debug.print("Native library not available, skipping test\n", .{});
            return;
        }
        return err;
    };
    defer hasher.deinit();

    // Check SIMD availability
    const has_simd = hasher.hasSimd();
    const impl_name = hasher.getImplInfo();
    std.debug.print("SIMD: {any}, Impl: {s}\n", .{ has_simd, impl_name });
}
