use img_hash::{HashAlg, HasherConfig};

/// Fallible variant used by command/vault integration paths.
pub fn generate_phash_checked(image_bytes: &[u8]) -> Result<String, String> {
    let image = decode_image_for_img_hash(image_bytes)?;
    let hasher = HasherConfig::new().hash_alg(HashAlg::Gradient).to_hasher();
    let hash = hasher.hash_image(&image);
    Ok(hex::encode(hash.as_bytes()))
}

fn decode_image_for_img_hash(image_bytes: &[u8]) -> Result<img_hash::image::DynamicImage, String> {
    let dynamic = image::load_from_memory(image_bytes)
        .map_err(|e| format!("decode image for perceptual hash: {e}"))?;
    let gray = dynamic.to_luma8();
    let (width, height) = gray.dimensions();
    let raw = gray.into_raw();
    let img_hash_gray = img_hash::image::ImageBuffer::<img_hash::image::Luma<u8>, Vec<u8>>::from_raw(
        width,
        height,
        raw,
    )
    .ok_or_else(|| "convert decoded image for perceptual hash".to_string())?;
    Ok(img_hash::image::DynamicImage::ImageLuma8(img_hash_gray))
}

/// Fallible distance function used by matching/search routines.
pub fn calculate_distance_checked(hash_a: &str, hash_b: &str) -> Result<u32, String> {
    let left = hex::decode(hash_a.trim()).map_err(|e| format!("parse first perceptual hash: {e}"))?;
    let right = hex::decode(hash_b.trim()).map_err(|e| format!("parse second perceptual hash: {e}"))?;
    if left.len() != right.len() {
        return Err("perceptual hash lengths do not match".to_string());
    }
    Ok(left
        .iter()
        .zip(right.iter())
        .map(|(a, b)| (a ^ b).count_ones())
        .sum())
}

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

    #[test]
    fn distance_for_identical_hash_is_zero() {
        let sample = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("..")
            .join("assets")
            .join("test_data")
            .join("sample.jpg");
        if !sample.exists() {
            return;
        }
        let bytes = std::fs::read(sample).expect("read sample");
        let hash = generate_phash_checked(&bytes).expect("hash sample");
        let distance = calculate_distance_checked(&hash, &hash).expect("distance");
        assert_eq!(distance, 0);
    }
}
