//! Content-Addressable Store (CAS) utilities.
//!
//! CAS lagrer filer med SHA-256-hash som nøkkel, organisert i
//! en to-nivå katalogstruktur: `{root}/{hash[0..2]}/{hash[2..4]}/{hash}`.
//!
//! Miljøvariabel: CAS_ROOT (default: /srv/synops/media/cas)

use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};

/// Standard CAS-rotkatalog.
pub const DEFAULT_CAS_ROOT: &str = "/srv/synops/media/cas";

/// Hent CAS-rotkatalog fra miljøvariabel eller bruk default.
pub fn root() -> String {
    std::env::var("CAS_ROOT").unwrap_or_else(|_| DEFAULT_CAS_ROOT.into())
}

/// Beregn filsti i CAS fra rot og hash.
///
/// Struktur: `{root}/{hash[0..2]}/{hash[2..4]}/{hash}`
///
/// # Eksempel
/// ```
/// use synops_common::cas;
/// let p = cas::path("/srv/synops/media/cas", "b94d27b9934d3e08");
/// assert_eq!(p.to_str().unwrap(), "/srv/synops/media/cas/b9/4d/b94d27b9934d3e08");
/// ```
pub fn path(root: &str, hash: &str) -> PathBuf {
    let (p1, rest) = hash.split_at(2.min(hash.len()));
    let (p2, _) = rest.split_at(2.min(rest.len()));
    PathBuf::from(root).join(p1).join(p2).join(hash)
}

/// Beregn SHA-256 hash av bytes. Returnerer hex-streng.
pub fn hash_bytes(data: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(data);
    hex::encode(hasher.finalize())
}

/// Beregn SHA-256 hash av en fil. Returnerer hex-streng.
pub async fn hash_file(file_path: &Path) -> Result<String, String> {
    let data = tokio::fs::read(file_path)
        .await
        .map_err(|e| format!("Kunne ikke lese fil {}: {e}", file_path.display()))?;
    Ok(hash_bytes(&data))
}

/// Lagre bytes i CAS. Returnerer hash. Oppretter kataloger ved behov.
pub async fn store(cas_root: &str, data: &[u8]) -> Result<String, String> {
    let hash = hash_bytes(data);
    let target = path(cas_root, &hash);

    if target.exists() {
        return Ok(hash);
    }

    if let Some(parent) = target.parent() {
        tokio::fs::create_dir_all(parent)
            .await
            .map_err(|e| format!("Kunne ikke opprette CAS-katalog: {e}"))?;
    }

    tokio::fs::write(&target, data)
        .await
        .map_err(|e| format!("Kunne ikke skrive CAS-fil: {e}"))?;

    Ok(hash)
}

/// Konverter MIME-type til filendelse.
pub fn mime_to_extension(mime: &str) -> &str {
    match mime {
        "audio/mpeg" | "audio/mp3" => "mp3",
        "audio/wav" | "audio/x-wav" => "wav",
        "audio/ogg" => "ogg",
        "audio/flac" | "audio/x-flac" => "flac",
        "audio/mp4" | "audio/m4a" | "audio/x-m4a" => "m4a",
        "audio/webm" => "webm",
        "video/mp4" => "mp4",
        "video/webm" => "webm",
        "image/jpeg" => "jpg",
        "image/png" => "png",
        "image/webp" => "webp",
        "image/gif" => "gif",
        "text/html" => "html",
        "application/pdf" => "pdf",
        _ => "bin",
    }
}

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

    #[test]
    fn cas_path_format() {
        let p = path("/srv/synops/media/cas", "b94d27b9934d3e08");
        assert_eq!(
            p,
            PathBuf::from("/srv/synops/media/cas/b9/4d/b94d27b9934d3e08")
        );
    }

    #[test]
    fn cas_path_short_hash() {
        let p = path("/cas", "ab");
        assert_eq!(p, PathBuf::from("/cas/ab//ab"));
    }

    #[test]
    fn hash_known_value() {
        let hash = hash_bytes(b"hello world");
        assert_eq!(
            hash,
            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
        );
    }

    #[test]
    fn mime_mapping() {
        assert_eq!(mime_to_extension("audio/mpeg"), "mp3");
        assert_eq!(mime_to_extension("image/png"), "png");
        assert_eq!(mime_to_extension("unknown/type"), "bin");
    }
}
