use serde_json::Value;
use std::fs;
use std::path::Path;

/// Default number of rolling backups to keep.
pub const BACKUP_COUNT: usize = 8;

/// Write JSON data to a file atomically with backup rotation.
///
/// Creates rolling backups (.bak1 through .bakN) and a .lastgood copy
/// for crash recovery.
///
/// For a file `foo.json`, backups are `foo.json.bak1`, `foo.json.tmp`, etc.
pub fn atomic_write_json(path: &Path, data: &Value, backup_count: usize) {
    // Create parent directories if needed
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).ok();
    }

    let path_str = path.as_os_str().to_string_lossy();
    let tmp = Path::new(&*format!("{}.tmp", path_str)).to_path_buf();
    let payload = serde_json::to_string_pretty(data).expect("failed to serialize JSON");

    if path.exists() {
        // Rotate existing backups: move .bakN -> .bak(N+1), down to .bak1 -> .bak2
        for i in (1..=backup_count).rev() {
            let src = Path::new(&*format!("{}.bak{}", path_str, i)).to_path_buf();
            let dst = Path::new(&*format!("{}.bak{}", path_str, i + 1)).to_path_buf();
            if src.exists() {
                // Remove dst if it exists, then rename src -> dst
                fs::remove_file(&dst).ok();
                fs::rename(&src, &dst).ok();
            }
        }

        // Move current file to .bak1
        let bak1 = Path::new(&*format!("{}.bak1", path_str)).to_path_buf();
        fs::remove_file(&bak1).ok();
        fs::rename(path, &bak1).ok();
    }

    // Write atomically via tmp file
    fs::write(&tmp, &payload).expect("failed to write tmp file");
    fs::rename(&tmp, path).expect("failed to rename tmp to primary");

    // Keep a .lastgood copy for recovery
    let lastgood = Path::new(&*format!("{}.lastgood", path_str)).to_path_buf();
    fs::write(&lastgood, &payload).ok();
}

/// Load JSON from path, falling back to backups if the primary is corrupted.
///
/// Tries: path -> .lastgood -> .bak1 -> .bak2 -> ... -> .bak{backup_count+2}
/// Returns `None` if all candidates fail.
pub fn load_json_with_fallbacks(path: &Path, backup_count: usize) -> Option<Value> {
    let path_str = path.as_os_str().to_string_lossy();

    // Build candidate list: primary, lastgood, bak1..bak{backup_count+2}
    let mut candidates: Vec<std::path::PathBuf> = Vec::new();
    candidates.push(path.to_path_buf());
    candidates.push(Path::new(&*format!("{}.lastgood", path_str)).to_path_buf());
    for i in 1..=(backup_count + 2) {
        candidates.push(Path::new(&*format!("{}.bak{}", path_str, i)).to_path_buf());
    }

    for p in &candidates {
        if p.exists() {
            if let Ok(text) = fs::read_to_string(p) {
                if let Ok(val) = serde_json::from_str::<Value>(&text) {
                    return Some(val);
                }
            }
        }
    }

    None
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;
    use tempfile::TempDir;

    /// Helper: path to a JSON file inside a temp dir.
    fn json_path(dir: &TempDir) -> std::path::PathBuf {
        dir.path().join("data.json")
    }

    #[test]
    fn test_write_and_read_round_trip() {
        let dir = TempDir::new().unwrap();
        let path = json_path(&dir);
        let data = json!({"key": "value", "num": 42});

        atomic_write_json(&path, &data, BACKUP_COUNT);

        let loaded = load_json_with_fallbacks(&path, BACKUP_COUNT);
        assert_eq!(loaded, Some(data));
    }

    #[test]
    fn test_fallback_to_lastgood_when_primary_corrupted() {
        let dir = TempDir::new().unwrap();
        let path = json_path(&dir);
        let data = json!({"status": "good"});

        // Write valid data (creates primary + lastgood)
        atomic_write_json(&path, &data, BACKUP_COUNT);

        // Corrupt the primary file
        fs::write(&path, "NOT VALID JSON!!!").unwrap();

        let loaded = load_json_with_fallbacks(&path, BACKUP_COUNT);
        assert_eq!(loaded, Some(data));
    }

    #[test]
    fn test_fallback_to_bak1_when_primary_and_lastgood_corrupted() {
        let dir = TempDir::new().unwrap();
        let path = json_path(&dir);
        let path_str = path.as_os_str().to_string_lossy().to_string();

        let first = json!({"version": 1});
        let second = json!({"version": 2});

        // First write — creates primary + lastgood
        atomic_write_json(&path, &first, BACKUP_COUNT);
        // Second write — rotates first to .bak1, writes second as primary + lastgood
        atomic_write_json(&path, &second, BACKUP_COUNT);

        // Corrupt primary and lastgood
        fs::write(&path, "CORRUPT").unwrap();
        let lastgood = format!("{}.lastgood", path_str);
        fs::write(&lastgood, "ALSO CORRUPT").unwrap();

        // Should fall back to .bak1 which has first version
        let loaded = load_json_with_fallbacks(&path, BACKUP_COUNT);
        assert_eq!(loaded, Some(first));
    }

    #[test]
    fn test_backup_rotation_after_multiple_writes() {
        let dir = TempDir::new().unwrap();
        let path = json_path(&dir);
        let path_str = path.as_os_str().to_string_lossy().to_string();

        // Write 5 times with distinct values
        for i in 1..=5 {
            let data = json!({"write": i});
            atomic_write_json(&path, &data, BACKUP_COUNT);
        }

        // Primary should be the latest (write 5)
        let primary: Value =
            serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
        assert_eq!(primary, json!({"write": 5}));

        // .bak1 should be the second-to-last (write 4)
        let bak1_path = format!("{}.bak1", path_str);
        let bak1: Value =
            serde_json::from_str(&fs::read_to_string(&bak1_path).unwrap()).unwrap();
        assert_eq!(bak1, json!({"write": 4}));

        // .bak2 should be write 3
        let bak2_path = format!("{}.bak2", path_str);
        let bak2: Value =
            serde_json::from_str(&fs::read_to_string(&bak2_path).unwrap()).unwrap();
        assert_eq!(bak2, json!({"write": 3}));

        // .bak3 should be write 2
        let bak3_path = format!("{}.bak3", path_str);
        let bak3: Value =
            serde_json::from_str(&fs::read_to_string(&bak3_path).unwrap()).unwrap();
        assert_eq!(bak3, json!({"write": 2}));

        // .bak4 should be write 1
        let bak4_path = format!("{}.bak4", path_str);
        let bak4: Value =
            serde_json::from_str(&fs::read_to_string(&bak4_path).unwrap()).unwrap();
        assert_eq!(bak4, json!({"write": 1}));
    }

    #[test]
    fn test_load_nonexistent_returns_none() {
        let dir = TempDir::new().unwrap();
        let path = dir.path().join("does_not_exist.json");

        let loaded = load_json_with_fallbacks(&path, BACKUP_COUNT);
        assert_eq!(loaded, None);
    }

    #[test]
    fn test_parent_directories_created() {
        let dir = TempDir::new().unwrap();
        let path = dir.path().join("sub").join("dir").join("nested.json");
        let data = json!({"nested": true});

        atomic_write_json(&path, &data, BACKUP_COUNT);

        let loaded = load_json_with_fallbacks(&path, BACKUP_COUNT);
        assert_eq!(loaded, Some(data));
    }

    #[test]
    fn test_lastgood_written() {
        let dir = TempDir::new().unwrap();
        let path = json_path(&dir);
        let path_str = path.as_os_str().to_string_lossy().to_string();
        let data = json!({"lg": true});

        atomic_write_json(&path, &data, BACKUP_COUNT);

        let lastgood_path = format!("{}.lastgood", path_str);
        let lg: Value =
            serde_json::from_str(&fs::read_to_string(&lastgood_path).unwrap()).unwrap();
        assert_eq!(lg, data);
    }

    #[test]
    fn test_backup_count_respected() {
        let dir = TempDir::new().unwrap();
        let path = json_path(&dir);
        let path_str = path.as_os_str().to_string_lossy().to_string();
        let small_count = 2;

        // Write 5 times with a backup_count of 2
        for i in 1..=5 {
            let data = json!({"write": i});
            atomic_write_json(&path, &data, small_count);
        }

        // With backup_count=2, rotation only goes up to .bak2 -> .bak3
        // After 5 writes: primary=5, bak1=4, bak2=3, bak3=2 (pushed from bak2)
        // .bak1 should exist
        let bak1_path = format!("{}.bak1", path_str);
        assert!(Path::new(&bak1_path).exists());

        // .bak2 should exist
        let bak2_path = format!("{}.bak2", path_str);
        assert!(Path::new(&bak2_path).exists());

        // .bak3 should exist (rotated from bak2)
        let bak3_path = format!("{}.bak3", path_str);
        assert!(Path::new(&bak3_path).exists());
    }
}
