const std = @import("std");
const ignore = @import("ignore.zig");

pub fn encodeHexLower(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 {
    const hex = try allocator.alloc(u8, bytes.len * 2);
    for (bytes, 0..) |byte, i| {
        const hi: u8 = (byte >> 4) & 0xf;
        const lo: u8 = byte & 0xf;
        hex[i * 2] = if (hi < 10) '0' + hi else 'a' + (hi - 10);
        hex[i * 2 + 1] = if (lo < 10) '0' + lo else 'a' + (lo - 10);
    }
    return hex;
}

fn hexNibble(c: u8) ?u8 {
    return if (c >= '0' and c <= '9') c - '0' else if (c >= 'a' and c <= 'f') c - 'a' + 10 else if (c >= 'A' and c <= 'F') c - 'A' + 10 else null;
}

pub fn decodeHex(allocator: std.mem.Allocator, hex: []const u8) ![]u8 {
    if ((hex.len % 2) != 0) return error.InvalidHex;
    const out = try allocator.alloc(u8, hex.len / 2);
    errdefer allocator.free(out);
    var i: usize = 0;
    while (i < out.len) : (i += 1) {
        const hi = hexNibble(hex[i * 2]) orelse return error.InvalidHex;
        const lo = hexNibble(hex[i * 2 + 1]) orelse return error.InvalidHex;
        out[i] = (hi << 4) | lo;
    }
    return out;
}

/// Hash a string using SHA256 and return lowercase hex string
pub fn hashString(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
    var hash: [32]u8 = undefined;
    std.crypto.hash.sha2.Sha256.hash(input, &hash, .{});
    return encodeHexLower(allocator, &hash);
}

/// Hash an API key using SHA256 and return first 16 bytes (binary)
pub fn hashApiKey(allocator: std.mem.Allocator, api_key: []const u8) ![]u8 {
    var hash: [32]u8 = undefined;
    std.crypto.hash.sha2.Sha256.hash(api_key, &hash, .{});

    // Return first 16 bytes
    const result = try allocator.alloc(u8, 16);
    @memcpy(result, hash[0..16]);
    return result;
}

/// Calculate SHA256 hash of a file
pub fn hashFile(allocator: std.mem.Allocator, file_path: []const u8) ![]u8 {
    var hasher = std.crypto.hash.sha2.Sha256.init(.{});

    const file = try std.fs.cwd().openFile(file_path, .{});
    defer file.close();

    var buf: [4096]u8 = undefined;
    while (true) {
        const bytes_read = try file.read(&buf);
        if (bytes_read == 0) break;
        hasher.update(buf[0..bytes_read]);
    }

    var hash: [32]u8 = undefined;
    hasher.final(&hash);
    return encodeHexLower(allocator, &hash);
}

/// Calculate combined hash of multiple files (sorted by path)
pub fn hashFiles(allocator: std.mem.Allocator, dir_path: []const u8, file_paths: []const []const u8) ![]u8 {
    var hasher = std.crypto.hash.sha2.Sha256.init(.{});

    // Copy and sort paths for deterministic hashing
    var sorted_paths = std.ArrayList([]const u8).initCapacity(allocator, file_paths.len) catch |err| {
        return err;
    };
    defer sorted_paths.deinit(allocator);

    for (file_paths) |path| {
        try sorted_paths.append(allocator, path);
    }

    std.sort.block([]const u8, sorted_paths.items, {}, struct {
        fn lessThan(_: void, a: []const u8, b: []const u8) bool {
            return std.mem.order(u8, a, b) == .lt;
        }
    }.lessThan);

    // Hash each file
    for (sorted_paths.items) |path| {
        hasher.update(path);
        hasher.update(&[_]u8{0}); // Separator

        const full_path = try std.fs.path.join(allocator, &[_][]const u8{ dir_path, path });
        defer allocator.free(full_path);

        const file_hash = try hashFile(allocator, full_path);
        defer allocator.free(file_hash);

        hasher.update(file_hash);
        hasher.update(&[_]u8{0}); // Separator
    }

    var hash: [32]u8 = undefined;
    hasher.final(&hash);
    return encodeHexLower(allocator, &hash);
}

/// Calculate commit ID for a directory (SHA256 of tree state, respecting .gitignore)
pub fn hashDirectory(allocator: std.mem.Allocator, dir_path: []const u8) ![]u8 {
    var hasher = std.crypto.hash.sha2.Sha256.init(.{});

    var dir = try std.fs.cwd().openDir(dir_path, .{ .iterate = true });
    defer dir.close();

    // Load .gitignore and .mlignore patterns
    var gitignore = ignore.GitIgnore.init(allocator);
    defer gitignore.deinit();

    try gitignore.loadFromDir(dir_path, ".gitignore");
    try gitignore.loadFromDir(dir_path, ".mlignore");

    var walker = try dir.walk(allocator);
    defer walker.deinit(allocator);

    // Collect and sort paths for deterministic hashing
    var paths: std.ArrayList([]const u8) = .{};
    defer {
        for (paths.items) |path| allocator.free(path);
        paths.deinit(allocator);
    }

    while (try walker.next()) |entry| {
        if (entry.kind == .file) {
            // Skip files matching default ignores
            if (ignore.matchesDefaultIgnore(entry.path)) continue;

            // Skip files matching .gitignore/.mlignore patterns
            if (gitignore.isIgnored(entry.path, false)) continue;

            try paths.append(allocator, try allocator.dupe(u8, entry.path));
        }
    }

    std.sort.block([]const u8, paths.items, {}, struct {
        fn lessThan(_: void, a: []const u8, b: []const u8) bool {
            return std.mem.order(u8, a, b) == .lt;
        }
    }.lessThan);

    // Hash each file path and content
    for (paths.items) |path| {
        hasher.update(path);
        hasher.update(&[_]u8{0}); // Separator

        const file = try dir.openFile(path, .{});
        defer file.close();

        var buf: [4096]u8 = undefined;
        while (true) {
            const bytes_read = try file.read(&buf);
            if (bytes_read == 0) break;
            hasher.update(buf[0..bytes_read]);
        }
        hasher.update(&[_]u8{0}); // Separator
    }

    var hash: [32]u8 = undefined;
    hasher.final(&hash);
    return encodeHexLower(allocator, &hash);
}

test "hash string" {
    const allocator = std.testing.allocator;

    const hash = try hashString(allocator, "test");
    defer allocator.free(hash);

    // SHA256 of "test"
    try std.testing.expectEqualStrings("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", hash);
}

test "hash empty string" {
    const allocator = std.testing.allocator;

    const hash = try hashString(allocator, "");
    defer allocator.free(hash);

    // SHA256 of empty string
    try std.testing.expectEqualStrings("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hash);
}

test "hash directory" {
    const allocator = std.testing.allocator;

    // For now, just test that we can hash the current directory
    // This should work if there are files in the current directory
    const hash = try hashDirectory(allocator, ".");
    defer allocator.free(hash);

    // Should produce a valid 64-character hex string
    try std.testing.expectEqual(@as(usize, 64), hash.len);

    // All characters should be valid hex
    for (hash) |c| {
        try std.testing.expect((c >= '0' and c <= '9') or (c >= 'a' and c <= 'f'));
    }
}

test "hex encode/decode roundtrip" {
    const allocator = std.testing.allocator;

    const bytes = [_]u8{ 0x00, 0x01, 0x7f, 0x80, 0xfe, 0xff };
    const enc = try encodeHexLower(allocator, &bytes);
    defer allocator.free(enc);
    try std.testing.expectEqualStrings("00017f80feff", enc);

    const dec = try decodeHex(allocator, enc);
    defer allocator.free(dec);
    try std.testing.expectEqualSlices(u8, &bytes, dec);
}

test "hex decode rejects invalid" {
    const allocator = std.testing.allocator;

    try std.testing.expectError(error.InvalidHex, decodeHex(allocator, "0"));
    try std.testing.expectError(error.InvalidHex, decodeHex(allocator, "zz"));
}
