//! Hash-based shard assignment for parallel file operations.

#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
use std::path::Path;

use rapidhash::v3::rapidhash_v3;

/// rapidhash of a path's raw bytes (lossy UTF-8 on non-unix).
fn rapidhash_path(path: &Path) -> u64 {
    #[cfg(unix)]
    let bytes = path.as_os_str().as_bytes();
    #[cfg(not(unix))]
    let lossy = path.as_os_str().to_string_lossy();
    #[cfg(not(unix))]
    let bytes = lossy.as_bytes();
    rapidhash_v3(bytes)
}

/// Compute the shard index for a path based on its parent directory.
///
/// Files in the same directory will always be assigned to the same shard,
/// which avoids lock contention when creating parent directories.
pub(crate) fn shard_for_path(path: &Path, num_shards: usize) -> usize {
    let parent = path.parent().unwrap_or(path);
    (rapidhash_path(parent) as usize) % num_shards
}

/// Deterministic 16-hex-char (full 64-bit) hash of a path's full bytes.
///
/// Disambiguates same-basename worktrees that share a basename-derived key (btrfs
/// snapshot name, worktree DB id). Full 64 bits keep a collision astronomically
/// unlikely.
#[cfg(any(target_os = "linux", feature = "metadata"))]
pub(crate) fn short_path_hash(path: &Path) -> String {
    format!("{:016x}", rapidhash_path(path))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    #[test]
    fn test_same_directory_same_shard() {
        let file1 = PathBuf::from("src/foo.rs");
        let file2 = PathBuf::from("src/bar.rs");
        let file3 = PathBuf::from("src/baz.rs");

        let num_shards = 8;

        let shard1 = shard_for_path(&file1, num_shards);
        let shard2 = shard_for_path(&file2, num_shards);
        let shard3 = shard_for_path(&file3, num_shards);

        // All files in src/ should go to the same shard
        assert_eq!(shard1, shard2);
        assert_eq!(shard2, shard3);
    }

    #[test]
    fn test_different_directories_may_differ() {
        let file1 = PathBuf::from("src/foo.rs");
        let file2 = PathBuf::from("tests/foo.rs");

        let num_shards = 8;

        // Different directories may (but don't have to) produce different shards
        let _shard1 = shard_for_path(&file1, num_shards);
        let _shard2 = shard_for_path(&file2, num_shards);
        // Just verify it doesn't panic
    }

    #[test]
    fn test_shard_in_range() {
        let path = PathBuf::from("some/deep/nested/path/file.txt");

        for num_shards in 1..=16 {
            let shard = shard_for_path(&path, num_shards);
            assert!(shard < num_shards);
        }
    }

    #[test]
    fn test_root_file() {
        let path = PathBuf::from("file.txt");
        let shard = shard_for_path(&path, 8);
        assert!(shard < 8);
    }
}
