//! IO utility functions — clean wrappers for common file system operations.
//!
//! Inspired by Apache Commons IO: eliminate the boilerplate of Rust's
//! OsString/OsStr conversions, path traversal checks, and error context.

use std::ffi::OsString;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;

/// Structured disk usage information from `df`.
pub struct DiskUsage {
    pub total_mb: u64,
    pub used_mb: u64,
    pub available_mb: u64,
    pub use_percent: u8,
}

/// Query disk usage at the given path. Returns structured disk info.
///
/// Wraps `df -BM --output=size,used,avail,pcent` and parses the output.
/// Returns `None` if the command fails or output cannot be parsed.
pub(crate) fn disk_usage(path: &Path) -> Option<DiskUsage> {
    let output = Command::new("df")
        .args(["-BM", "--output=size,used,avail,pcent"])
        .arg(path)
        .output()
        .ok()?;

    if !output.status.success() {
        return None;
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let line = stdout.lines().nth(1)?;
    let fields: Vec<&str> = line.split_whitespace().collect();
    if fields.len() < 4 {
        return None;
    }

    let total_mb = fields[0].trim_end_matches('M').parse::<u64>().ok()?;
    let used_mb = fields[1].trim_end_matches('M').parse::<u64>().ok()?;
    let available_mb = fields[2].trim_end_matches('M').parse::<u64>().ok()?;
    let use_percent = fields[3].trim_end_matches('%').parse::<u8>().ok()?;

    Some(DiskUsage {
        total_mb,
        used_mb,
        available_mb,
        use_percent,
    })
}

/// Convert OsString to String, handling non-UTF8 filenames with lossy conversion.
pub fn os_to_string(os: OsString) -> String {
    os.to_string_lossy().into_owned()
}

/// Extract the file name component from a path as String (lossy conversion).
///
/// Returns empty string if the path has no file name component (e.g. root `/`).
pub fn file_name_string(path: &Path) -> String {
    path.file_name()
        .map(|s| s.to_string_lossy().into_owned())
        .unwrap_or_default()
}

/// Convert a Path to String with lossy conversion.
pub fn path_to_string(path: &Path) -> String {
    path.to_string_lossy().into_owned()
}

/// Resolve a relative path within a base directory, with path traversal protection.
///
/// Canonicalizes both the base and the resolved path, then verifies the resolved
/// path is within (starts with) the canonical base. Returns an IO error with
/// `PermissionDenied` kind if the path escapes the base directory.
///
/// If `relative` is empty, returns the canonical base directory itself.
pub(crate) fn safe_resolve(base: &Path, relative: &str) -> crate::Critical<PathBuf> {
    use crate::IntoCritical;
    (|| -> io::Result<PathBuf> {
        let base_canonical = base.canonicalize()?;
        if relative.is_empty() {
            return Ok(base_canonical);
        }
        let target = base.join(relative).canonicalize()?;
        if !target.starts_with(&base_canonical) {
            return Err(io::Error::new(
                io::ErrorKind::PermissionDenied,
                format!("path traversal denied: {}", relative),
            ));
        }
        Ok(target)
    })().into_critical("safe_resolve")
}

/// Read a file to string, with the file path included in error messages.
///
/// Unlike `std::fs::read_to_string`, the returned error includes the path
/// that failed, making debugging easier.
pub(crate) fn read_to_string(path: impl AsRef<Path>) -> crate::Critical<String> {
    use crate::IntoCritical;
    let path = path.as_ref();
    fs::read_to_string(path).map_err(|e| {
        io::Error::new(e.kind(), format!("{}: {}", path_to_string(path), e))
    }).into_critical("ioutil::read_to_string")
}

/// Get metadata for a directory entry, following symlinks.
///
/// Uses `std::fs::metadata` (which follows symlinks) with fallback to
/// `DirEntry::metadata` (which may not follow symlinks on all platforms).
pub(crate) fn entry_metadata(entry: &fs::DirEntry) -> crate::Critical<fs::Metadata> {
    use crate::IntoCritical;
    fs::metadata(entry.path()).or_else(|_| entry.metadata())
        .into_critical("entry_metadata")
}
/// List non-hidden directory names under `path`.
///
/// Skips entries starting with '.', non-directory entries, and unreadable entries.
pub(crate) fn list_dirs(path: &Path) -> crate::Critical<Vec<String>> {
    use crate::IntoCritical;
    (|| -> io::Result<Vec<String>> {
        let mut dirs = Vec::new();
        for entry in fs::read_dir(path)? {
            let entry = match entry {
                Ok(e) => e,
                Err(_) => continue,
            };
            let name = os_to_string(entry.file_name());
            if name.starts_with('.') {
                continue;
            }
            let meta = fs::metadata(entry.path()).or_else(|_| entry.metadata());
            match meta {
                Ok(m) if m.is_dir() => dirs.push(name),
                _ => continue,
            }
        }
        Ok(dirs)
    })().into_critical("list_dirs")
}

/// Convert kilobytes to bytes.
pub const fn kb_to_bytes(kb: u64) -> u64 {
    kb * 1024
}

/// Convert bytes to kilobytes (integer division).
pub const fn bytes_to_kb(bytes: u64) -> u64 {
    bytes / 1024
}

/// Convert megabytes to bytes.
pub const fn mb_to_bytes(mb: u64) -> u64 {
    mb * 1024 * 1024
}

/// Exponential backoff: `min(base * 2^(min(count-1, max_exp)), cap)`.
///
/// Returns 0 if `count` is zero.
pub fn exponential_backoff(count: u32, base: u64, max_exponent: u32, cap: u64) -> u64 {
    if count == 0 {
        return 0;
    }
    let exp = (count - 1).min(max_exponent);
    std::cmp::min(base * (1u64 << exp), cap)
}

/// Move a subdirectory to a `.trash` directory under the same parent.
///
/// Creates the `.trash` directory if it doesn't exist. The trashed entry
/// is renamed to `{source_name}_{timestamp}` to avoid collisions.
///
/// Returns the trash entry name.
pub(crate) fn move_to_trash(parent_dir: &Path, source_name: &str) -> crate::Critical<String> {
    use crate::IntoCritical;
    (|| -> io::Result<String> {
        let trash_dir = parent_dir.join(".trash");
        fs::create_dir_all(&trash_dir)?;
        let trash_name = format!(
            "{}_{}",
            source_name,
            crate::timestamp::Timestamp::now()
        );
        let trash_path = trash_dir.join(&trash_name);
        fs::rename(parent_dir.join(source_name), &trash_path)?;
        Ok(trash_name)
    })().into_critical("move_to_trash")
}