/// Number of slots in a Redis Cluster.
pub const REDIS_CLUSTER_SLOTS: u16 = 16_384;

/// Returns the Redis Cluster hash tag used for slot calculation.
///
/// Redis only uses a hash tag when the first `{` is followed by a non-empty
/// substring before `}`. Otherwise the full key is hashed.
pub fn redis_cluster_hash_tag(key: &str) -> &str {
    let Some(start) = key.find('{') else {
        return key;
    };
    let rest = &key[start + 1..];
    let Some(end) = rest.find('}') else {
        return key;
    };
    if end == 0 { key } else { &rest[..end] }
}

/// Calculates the Redis Cluster slot for a key.
pub fn redis_cluster_slot(key: &str) -> u16 {
    crc16(redis_cluster_hash_tag(key).as_bytes()) % REDIS_CLUSTER_SLOTS
}

fn crc16(bytes: &[u8]) -> u16 {
    let mut crc = 0u16;
    for byte in bytes {
        crc ^= (*byte as u16) << 8;
        for _ in 0..8 {
            if crc & 0x8000 != 0 {
                crc = (crc << 1) ^ 0x1021;
            } else {
                crc <<= 1;
            }
        }
    }
    crc
}

#[cfg(test)]
mod tests {
    use super::{redis_cluster_hash_tag, redis_cluster_slot};

    #[test]
    fn slot_matches_redis_examples() {
        assert_eq!(redis_cluster_slot("123456789"), 12739);
        assert_eq!(redis_cluster_slot("{user1000}.following"), 3443);
        assert_eq!(redis_cluster_slot("{user1000}.followers"), 3443);
    }

    #[test]
    fn hash_tag_requires_non_empty_tag() {
        assert_eq!(redis_cluster_hash_tag("foo{}{bar}"), "foo{}{bar}");
        assert_eq!(redis_cluster_hash_tag("foo{bar}zap"), "bar");
    }
}
