const std = @import("std");

/// Embedded rsync binary functionality
pub const EmbeddedRsync = struct {
    allocator: std.mem.Allocator,

    const Self = @This();

    fn encodeHexLower(out: []u8, bytes: []const u8) void {
        const hex = "0123456789abcdef";
        var i: usize = 0;
        while (i < bytes.len) : (i += 1) {
            const b = bytes[i];
            out[i * 2] = hex[(b >> 4) & 0x0f];
            out[i * 2 + 1] = hex[b & 0x0f];
        }
    }

    fn cacheBaseDir(self: Self) ![]u8 {
        if (std.posix.getenv("XDG_CACHE_HOME")) |z| {
            return self.allocator.dupe(u8, std.mem.sliceTo(z, 0));
        }

        const home = if (std.posix.getenv("HOME")) |z| std.mem.sliceTo(z, 0) else return error.MissingHome;
        const target = @import("builtin").target;
        if (target.os.tag == .macos) {
            return std.fmt.allocPrint(self.allocator, "{s}/Library/Caches", .{home});
        }
        return std.fmt.allocPrint(self.allocator, "{s}/.cache", .{home});
    }

    fn resolveRsyncPathOverride(self: Self) !?[]u8 {
        const z = std.posix.getenv("ML_RSYNC_PATH") orelse return null;
        const p = std.mem.sliceTo(z, 0);
        if (p.len == 0) return null;

        // Basic validation: must exist and be executable.
        const file = std.fs.openFileAbsolute(p, .{}) catch return error.InvalidRsyncPath;
        defer file.close();
        const stat = try file.stat();
        if (stat.mode & 0o111 == 0) return error.InvalidRsyncPath;

        const dup = try self.allocator.dupe(u8, p);
        return @as(?[]u8, dup);
    }

    fn resolveSystemRsyncPath(self: Self) !?[]u8 {
        const path_z = std.posix.getenv("PATH") orelse return null;
        const path = std.mem.sliceTo(path_z, 0);

        var it = std.mem.splitScalar(u8, path, ':');
        while (it.next()) |dir| {
            if (dir.len == 0) continue;
            const candidate = try std.fmt.allocPrint(self.allocator, "{s}/{s}", .{ dir, "rsync" });
            errdefer self.allocator.free(candidate);

            const file = std.fs.openFileAbsolute(candidate, .{}) catch {
                self.allocator.free(candidate);
                continue;
            };
            defer file.close();

            const stat = file.stat() catch {
                self.allocator.free(candidate);
                continue;
            };

            if (stat.mode & 0o111 == 0) {
                self.allocator.free(candidate);
                continue;
            }

            return @as(?[]u8, candidate);
        }

        return null;
    }

    /// Extract embedded rsync binary to temporary location
    pub fn extractRsyncBinary(self: Self) ![]const u8 {
        if (try self.resolveRsyncPathOverride()) |p| {
            return p;
        }

        if (try self.resolveSystemRsyncPath()) |p| {
            return p;
        }

        const embedded_binary = @import("rsync_embedded_binary.zig");
        const digest = embedded_binary.rsyncBinarySha256();

        var digest_hex: [64]u8 = undefined;
        encodeHexLower(digest_hex[0..], digest[0..]);

        const cache_base = try self.cacheBaseDir();
        defer self.allocator.free(cache_base);

        const cache_dir = try std.fmt.allocPrint(self.allocator, "{s}/fetchml/rsync", .{cache_base});
        defer self.allocator.free(cache_dir);

        std.fs.cwd().makePath(cache_dir) catch |err| switch (err) {
            error.PathAlreadyExists => {},
            else => return err,
        };

        const rsync_path = try std.fmt.allocPrint(self.allocator, "{s}/ml_rsync_{s}", .{ cache_dir, digest_hex[0..16] });
        errdefer self.allocator.free(rsync_path);

        // If file exists and matches the embedded digest, reuse it.
        if (std.fs.openFileAbsolute(rsync_path, .{})) |file| {
            defer file.close();

            const stat = try file.stat();
            if (stat.mode & 0o111 != 0) {
                const data = try file.readToEndAlloc(self.allocator, 1024 * 1024 * 4);
                defer self.allocator.free(data);

                var on_disk: [32]u8 = undefined;
                std.crypto.hash.sha2.Sha256.hash(data, &on_disk, .{});
                if (std.mem.eql(u8, on_disk[0..], digest[0..])) {
                    return rsync_path;
                }
            }
        } else |_| {
            // Not present; will extract.
        }

        try self.extractAndSetExecutable(rsync_path);
        return rsync_path;
    }

    /// Extract rsync binary from embedded data and set executable permissions
    fn extractAndSetExecutable(self: Self, path: []const u8) !void {
        // Import embedded binary data
        const embedded_binary = @import("rsync_embedded_binary.zig");

        // Get the embedded rsync binary data
        const binary_data = embedded_binary.getRsyncBinary();

        // Debug output to show we're using embedded binary
        const debug_msg = try std.fmt.allocPrint(self.allocator, "Extracting embedded rsync binary to {s} (size: {d} bytes)", .{ path, binary_data.len });
        defer self.allocator.free(debug_msg);
        std.log.debug("{s}", .{debug_msg});

        // Write embedded binary to file system (absolute path)
        var file = try std.fs.createFileAbsolute(path, .{ .truncate = true, .read = false, .mode = 0o755 });
        defer file.close();
        try file.writeAll(binary_data);

        // Ensure executable permissions
        const mode = 0o755; // rwxr-xr-x
        std.posix.fchmod(file.handle, mode) catch |err| {
            std.log.warn("Failed to set executable permissions on {s}: {}", .{ path, err });
        };
    }

    /// Sync using embedded rsync
    pub fn sync(self: Self, local_path: []const u8, remote_path: []const u8, ssh_port: u16) !void {
        const rsync_path = try self.extractRsyncBinary();
        defer self.allocator.free(rsync_path);

        const port_str = try std.fmt.allocPrint(self.allocator, "{d}", .{ssh_port});
        defer self.allocator.free(port_str);

        const ssh_opt = try std.fmt.allocPrint(self.allocator, "ssh -p {s}", .{port_str});
        defer self.allocator.free(ssh_opt);

        // Build rsync command using embedded binary
        var child = std.process.Child.init(
            &[_][]const u8{
                rsync_path,
                "-avz",
                "--delete",
                "-e",
                ssh_opt,
                local_path,
                remote_path,
            },
            self.allocator,
        );

        child.stdin_behavior = .Ignore;
        child.stdout_behavior = .Inherit;
        child.stderr_behavior = .Inherit;

        const term = child.spawnAndWait() catch |err| {
            std.log.err(
                "Failed to execute rsync at '{s}': {}. If your environment blocks executing extracted binaries (e.g. noexec), set ML_RSYNC_PATH to a system rsync.",
                .{ rsync_path, err },
            );
            return err;
        };

        switch (term) {
            .Exited => |code| {
                if (code != 0) {
                    std.debug.print("rsync failed with exit code {d}\n", .{code});
                    return error.RsyncFailed;
                }
            },
            .Signal => {
                return error.RsyncKilled;
            },
            else => {
                return error.RsyncUnknownError;
            },
        }
    }
};

/// Public sync function that uses embedded rsync
pub fn sync(allocator: std.mem.Allocator, local_path: []const u8, remote_path: []const u8, ssh_port: u16) !void {
    var embedded_rsync = EmbeddedRsync{ .allocator = allocator };
    try embedded_rsync.sync(local_path, remote_path, ssh_port);

    std.debug.print("Synced {s} to {s} using embedded rsync\n", .{ local_path, remote_path });
}
