use std::fs;
use std::path::Path;

use serde::de::DeserializeOwned;
use serde::Serialize;
use shared::AssetSourceFingerprint;

pub fn fingerprint_file(path: impl AsRef<Path>) -> Result<AssetSourceFingerprint, String> {
    let path = path.as_ref();
    let bytes = fs::read(path)
        .map_err(|error| format!("could not read imported source {}: {error}", path.display()))?;
    Ok(AssetSourceFingerprint::from_bytes(&bytes))
}

/// Writes canonical pretty RON only when the parsed document changes semantically.
///
/// Equivalent existing bytes, including custom formatting and final-newline policy, stay intact.
pub fn write_pretty_ron_if_changed<T>(path: impl AsRef<Path>, value: &T) -> Result<bool, String>
where
    T: DeserializeOwned + PartialEq + Serialize,
{
    let path = path.as_ref();
    if fs::read_to_string(path)
        .ok()
        .and_then(|text| ron::from_str::<T>(&text).ok())
        .is_some_and(|existing| existing == *value)
    {
        return Ok(false);
    }

    let text = ron::ser::to_string_pretty(value, ron::ser::PrettyConfig::default())
        .map_err(|error| format!("could not serialize RON: {error}"))?;
    if fs::read(path).ok().as_deref() == Some(text.as_bytes()) {
        return Ok(false);
    }
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|error| format!("could not create {}: {error}", parent.display()))?;
    }
    fs::write(path, text)
        .map_err(|error| format!("could not write {}: {error}", path.display()))?;
    Ok(true)
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde::Deserialize;
    use std::fs::{File, FileTimes};
    use std::time::{Duration, SystemTime};
    use uuid::Uuid;

    #[derive(Debug, Deserialize, PartialEq, Serialize)]
    struct Fixture {
        count: u32,
        label: String,
    }

    fn fixture_path(name: &str) -> std::path::PathBuf {
        std::env::temp_dir().join(format!(
            "blacksite-fingerprint-{name}-{}.ron",
            Uuid::new_v4()
        ))
    }

    #[test]
    fn metadata_only_drift_does_not_change_content_identity() {
        let path = fixture_path("mtime");
        fs::write(&path, b"stable source bytes").unwrap();
        let before = fingerprint_file(&path).unwrap();
        File::options()
            .write(true)
            .open(&path)
            .unwrap()
            .set_times(
                FileTimes::new().set_modified(SystemTime::now() + Duration::from_secs(86_400)),
            )
            .unwrap();

        assert_eq!(fingerprint_file(&path).unwrap(), before);
        fs::remove_file(path).unwrap();
    }

    #[test]
    fn same_size_byte_change_updates_content_identity() {
        let path = fixture_path("same-size");
        fs::write(&path, b"source-a").unwrap();
        let before = fingerprint_file(&path).unwrap();
        fs::write(&path, b"source-b").unwrap();
        let after = fingerprint_file(&path).unwrap();

        assert_eq!(before.byte_len, after.byte_len);
        assert_ne!(before.content_hash, after.content_hash);
        fs::remove_file(path).unwrap();
    }

    #[test]
    fn equivalent_ron_preserves_exact_existing_bytes() {
        let path = fixture_path("semantic");
        let existing = b"( label: \"stable\", count: 7, )\n\n";
        fs::write(&path, existing).unwrap();
        let value = Fixture {
            count: 7,
            label: "stable".into(),
        };

        assert!(!write_pretty_ron_if_changed(&path, &value).unwrap());
        assert_eq!(fs::read(&path).unwrap(), existing);
        fs::remove_file(path).unwrap();
    }
}
