use std::path::{Path, PathBuf};

fn backup_path_for(path: &Path) -> Option<PathBuf> {
    let filename = path.file_name()?.to_string_lossy();
    Some(path.with_file_name(format!("{filename}.bak")))
}

pub fn snapshot_mtime(path: &Path) -> Option<std::time::SystemTime> {
    std::fs::metadata(path).ok().and_then(|m| m.modified().ok())
}

pub fn write_atomic_with_backup(path: &Path, content: &str) -> Result<(), String> {
    write_atomic_with_backup_checked(path, content, None)
}

/// Remove stale timestamped `.bak` files left by the old backup scheme.
/// Called once at startup to clean up the accumulated backups.
pub fn cleanup_legacy_backups(data_dir: &Path) {
    let Ok(entries) = std::fs::read_dir(data_dir) else {
        return;
    };
    for entry in entries.flatten() {
        let name = entry.file_name();
        let name = name.to_string_lossy();
        if name.contains(".lean-ctx.") && name.ends_with(".bak") {
            let _ = std::fs::remove_file(entry.path());
        }
    }
}

pub fn write_atomic_with_backup_checked(
    path: &Path,
    content: &str,
    expected_mtime: Option<std::time::SystemTime>,
) -> Result<(), String> {
    if path.exists() {
        if let Some(expected) = expected_mtime {
            let current = snapshot_mtime(path);
            if current != Some(expected) {
                return Err(format!(
                    "file was modified externally since last read: {}",
                    path.display()
                ));
            }
        }
        if let Some(bak) = backup_path_for(path) {
            let _ = std::fs::copy(path, &bak);
        }
    }

    write_atomic(path, content)
}

pub fn write_atomic(path: &Path, content: &str) -> Result<(), String> {
    reject_symlink(path)?;

    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
    }

    let parent = path
        .parent()
        .ok_or_else(|| "invalid path (no parent directory)".to_string())?;
    let filename = path
        .file_name()
        .ok_or_else(|| "invalid path (no filename)".to_string())?
        .to_string_lossy();

    let pid = std::process::id();
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_or(0, |d| d.as_nanos());

    let tmp = parent.join(format!(".{filename}.lean-ctx.tmp.{pid}.{nanos}"));
    std::fs::write(&tmp, content).map_err(|e| e.to_string())?;

    #[cfg(windows)]
    {
        if path.exists() {
            let _ = std::fs::remove_file(path);
        }
    }

    std::fs::rename(&tmp, path).map_err(|e| {
        format!(
            "atomic write failed: {} (tmp: {})",
            e,
            tmp.to_string_lossy()
        )
    })?;

    restrict_file_permissions(path);

    Ok(())
}

fn reject_symlink(path: &Path) -> Result<(), String> {
    if path.exists()
        && path
            .symlink_metadata()
            .is_ok_and(|m| m.file_type().is_symlink())
    {
        return Err(format!(
            "refusing to write through symlink: {}",
            path.display()
        ));
    }
    Ok(())
}

#[cfg(unix)]
fn restrict_file_permissions(path: &Path) {
    use std::os::unix::fs::PermissionsExt;
    let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
}

#[cfg(not(unix))]
fn restrict_file_permissions(_path: &Path) {}
