//! Input validation helpers

use std::path::Path;
use std::time::Duration;

use super::error::ProfilingError;

/// Validate duration bounds
pub fn validate_duration(duration: Duration) -> Result<Duration, ProfilingError> {
    const MIN_DURATION: Duration = Duration::from_secs(1);
    const MAX_DURATION: Duration = Duration::from_secs(3600);

    if duration < MIN_DURATION {
        return Err(ProfilingError::InvalidConfig(
            "Duration must be at least 1 second".to_string(),
        ));
    }
    if duration > MAX_DURATION {
        return Err(ProfilingError::InvalidConfig(
            "Duration must not exceed 3600 seconds".to_string(),
        ));
    }

    Ok(duration)
}

/// Validate parallel stream count
pub fn validate_parallel_streams(streams: u32) -> Result<u32, ProfilingError> {
    const MIN_STREAMS: u32 = 1;
    const MAX_STREAMS: u32 = 128;

    if streams < MIN_STREAMS {
        return Err(ProfilingError::InvalidConfig(
            "Parallel streams must be at least 1".to_string(),
        ));
    }
    if streams > MAX_STREAMS {
        return Err(ProfilingError::InvalidConfig(
            "Parallel streams must not exceed 128".to_string(),
        ));
    }

    Ok(streams)
}

/// Validate filesystem path exists and is accessible
pub fn validate_path(path: &Path) -> Result<(), ProfilingError> {
    // Check for path traversal attempts
    let path_str = path.to_string_lossy();
    if path_str.contains("..") {
        return Err(ProfilingError::InvalidConfig(
            "Path traversal detected".to_string(),
        ));
    }

    if !path.exists() {
        return Err(ProfilingError::IoError(format!(
            "Path does not exist: {}",
            path.display()
        )));
    }

    if !path.is_dir() {
        return Err(ProfilingError::IoError(format!(
            "Path is not a directory: {}",
            path.display()
        )));
    }

    // Check write permissions
    let test_file = path.join(".herolib_profiling_test");
    match std::fs::write(&test_file, "test") {
        Ok(_) => {
            let _ = std::fs::remove_file(&test_file);
        }
        Err(e) => {
            return Err(ProfilingError::PermissionDenied(format!(
                "No write permission on path {}: {}",
                path.display(),
                e
            )));
        }
    }

    Ok(())
}

/// Validate file path exists and is accessible
pub fn validate_file_path(path: &Path) -> Result<(), ProfilingError> {
    // Check for path traversal attempts
    let path_str = path.to_string_lossy();
    if path_str.contains("..") {
        return Err(ProfilingError::InvalidConfig(
            "Path traversal detected".to_string(),
        ));
    }

    if !path.exists() {
        return Err(ProfilingError::IoError(format!(
            "File does not exist: {}",
            path.display()
        )));
    }

    if path.is_dir() {
        return Err(ProfilingError::IoError(format!(
            "Path is a directory, not a file: {}",
            path.display()
        )));
    }

    Ok(())
}

/// Validate server address for iperf3 (prevent command injection)
pub fn validate_server_address(address: &str) -> Result<String, ProfilingError> {
    let sanitized = address.trim();

    if sanitized.is_empty() {
        return Err(ProfilingError::InvalidConfig(
            "Server address cannot be empty".to_string(),
        ));
    }

    // Check for shell metacharacters
    let dangerous_chars = [';', '&', '|', '`', '$', '(', ')', '<', '>', '\n', '\r'];
    for char in dangerous_chars {
        if sanitized.contains(char) {
            return Err(ProfilingError::InvalidConfig(format!(
                "Server address contains invalid character: {}",
                char
            )));
        }
    }

    // Basic IP/hostname validation
    if !is_valid_hostname(sanitized) && !is_valid_ip(sanitized) {
        return Err(ProfilingError::InvalidConfig(
            "Invalid server address format".to_string(),
        ));
    }

    Ok(sanitized.to_string())
}

fn is_valid_hostname(hostname: &str) -> bool {
    if hostname.len() > 253 {
        return false;
    }

    let parts: Vec<&str> = hostname.split('.').collect();
    if parts.is_empty() {
        return false;
    }

    for part in parts {
        if part.is_empty() || part.len() > 63 {
            return false;
        }

        if !part.chars().all(|c| c.is_alphanumeric() || c == '-') {
            return false;
        }

        if part.starts_with('-') || part.ends_with('-') {
            return false;
        }
    }

    true
}

fn is_valid_ip(ip: &str) -> bool {
    ip.parse::<std::net::IpAddr>().is_ok()
}

/// Validate thread count for CPU benchmarks
pub fn validate_thread_count(threads: u32, available_threads: u32) -> Result<u32, ProfilingError> {
    if threads == 0 {
        return Err(ProfilingError::InvalidConfig(
            "Thread count must be at least 1".to_string(),
        ));
    }

    if threads > available_threads {
        return Err(ProfilingError::InvalidConfig(format!(
            "Thread count ({}) exceeds available threads ({})",
            threads, available_threads
        )));
    }

    Ok(threads)
}

/// Validate warm-up iteration count
pub fn validate_warmup_iterations(iterations: u32) -> Result<u32, ProfilingError> {
    const MAX_WARMUP: u32 = 10;

    if iterations > MAX_WARMUP {
        return Err(ProfilingError::InvalidConfig(format!(
            "Warm-up iterations must not exceed {}",
            MAX_WARMUP
        )));
    }

    Ok(iterations)
}

/// Validate memory allocation size
pub fn validate_memory_size(size_bytes: u64, max_allowed: u64) -> Result<u64, ProfilingError> {
    const MIN_MEMORY: u64 = 1024 * 1024; // 1MB minimum

    if size_bytes < MIN_MEMORY {
        return Err(ProfilingError::InvalidConfig(
            "Memory allocation must be at least 1MB".to_string(),
        ));
    }

    if size_bytes > max_allowed {
        return Err(ProfilingError::InsufficientSpace {
            required: size_bytes / (1024 * 1024),
            available: max_allowed / (1024 * 1024),
        });
    }

    Ok(size_bytes)
}

/// Validate port number
pub fn validate_port(port: u16) -> Result<u16, ProfilingError> {
    const MIN_PORT: u16 = 1;
    const MAX_PORT: u16 = 65535;

    if port < MIN_PORT {
        return Err(ProfilingError::InvalidConfig(
            "Port must be >= 1".to_string(),
        ));
    }

    if port > MAX_PORT {
        return Err(ProfilingError::InvalidConfig(
            "Port must be <= 65535".to_string(),
        ));
    }

    Ok(port)
}

/// Validate available disk space before running disk benchmarks
/// Returns Ok if sufficient space, Err with details if not
pub fn validate_disk_space(path: &Path, required_bytes: u64) -> Result<(), ProfilingError> {
    let available = get_available_disk_space(path)?;

    // Add 10% safety margin
    let required_with_margin = required_bytes + (required_bytes / 10);

    if available < required_with_margin {
        return Err(ProfilingError::InsufficientSpace {
            required: required_with_margin / (1024 * 1024),
            available: available / (1024 * 1024),
        });
    }

    Ok(())
}

/// Get available disk space for a path (cross-platform)
fn get_available_disk_space(path: &Path) -> Result<u64, ProfilingError> {
    #[cfg(unix)]
    {
        // Use nix safe wrapper for statvfs
        let stat = nix::sys::statvfs::statvfs(path).map_err(|e| {
            ProfilingError::IoError(format!("Failed to get filesystem stats: {}", e))
        })?;

        // Available space = blocks available * block size
        Ok(stat.blocks_available() as u64 * stat.block_size() as u64)
    }

    #[cfg(windows)]
    {
        // On Windows, use GetDiskFreeSpaceExW
        use std::os::windows::ffi::OsStrExt;

        let wide_path: Vec<u16> = path
            .as_os_str()
            .encode_wide()
            .chain(std::iter::once(0))
            .collect();

        let mut free_bytes: u64 = 0;
        let mut total_bytes: u64 = 0;
        let mut total_free_bytes: u64 = 0;

        unsafe {
            if windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(
                wide_path.as_ptr(),
                &mut free_bytes as *mut u64,
                &mut total_bytes as *mut u64,
                &mut total_free_bytes as *mut u64,
            ) == 0
            {
                return Err(ProfilingError::IoError(
                    "Failed to get disk space".to_string(),
                ));
            }
        }
        Ok(free_bytes)
    }

    #[cfg(not(any(unix, windows)))]
    {
        // Fallback: assume sufficient space
        Ok(u64::MAX)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::Duration;
    use tempfile::TempDir;

    #[test]
    fn test_validate_duration() {
        // Valid durations
        assert!(validate_duration(Duration::from_secs(1)).is_ok());
        assert!(validate_duration(Duration::from_secs(60)).is_ok());
        assert!(validate_duration(Duration::from_secs(3600)).is_ok());

        // Invalid durations
        assert!(validate_duration(Duration::from_millis(500)).is_err());
        assert!(validate_duration(Duration::from_secs(0)).is_err());
        assert!(validate_duration(Duration::from_secs(3601)).is_err());
        assert!(validate_duration(Duration::from_secs(10000)).is_err());
    }

    #[test]
    fn test_validate_parallel_streams() {
        // Valid stream counts
        assert!(validate_parallel_streams(1).is_ok());
        assert!(validate_parallel_streams(4).is_ok());
        assert!(validate_parallel_streams(128).is_ok());

        // Invalid stream counts
        assert!(validate_parallel_streams(0).is_err());
        assert!(validate_parallel_streams(129).is_err());
        assert!(validate_parallel_streams(1000).is_err());
    }

    #[test]
    fn test_validate_path() {
        // Valid path (temp directory)
        let temp_dir = TempDir::new().unwrap();
        assert!(validate_path(temp_dir.path()).is_ok());

        // Invalid path (doesn't exist)
        let non_existent = Path::new("/tmp/herolib_nonexistent_test_dir_12345");
        assert!(validate_path(non_existent).is_err());

        // Invalid path (path traversal)
        let traversal = Path::new("/tmp/../etc");
        assert!(validate_path(traversal).is_err());
    }

    #[test]
    fn test_validate_server_address() {
        // Valid addresses
        assert!(validate_server_address("localhost").is_ok());
        assert!(validate_server_address("127.0.0.1").is_ok());
        assert!(validate_server_address("example.com").is_ok());
        assert!(validate_server_address("192.168.1.1").is_ok());
        assert!(validate_server_address("test-server").is_ok());

        // Invalid addresses (command injection attempts)
        assert!(validate_server_address("localhost; rm -rf /").is_err());
        assert!(validate_server_address("localhost && echo pwned").is_err());
        assert!(validate_server_address("localhost | cat /etc/passwd").is_err());
        assert!(validate_server_address("localhost`whoami`").is_err());
        assert!(validate_server_address("localhost$(whoami)").is_err());

        // Invalid addresses (empty)
        assert!(validate_server_address("").is_err());
        assert!(validate_server_address("   ").is_err());
    }

    #[test]
    fn test_validate_thread_count() {
        // Valid thread counts
        assert!(validate_thread_count(1, 8).is_ok());
        assert!(validate_thread_count(4, 8).is_ok());
        assert!(validate_thread_count(8, 8).is_ok());

        // Invalid thread counts
        assert!(validate_thread_count(0, 8).is_err());
        assert!(validate_thread_count(9, 8).is_err());
        assert!(validate_thread_count(100, 8).is_err());
    }

    #[test]
    fn test_validate_warmup_iterations() {
        // Valid warmup counts
        assert!(validate_warmup_iterations(0).is_ok());
        assert!(validate_warmup_iterations(5).is_ok());
        assert!(validate_warmup_iterations(10).is_ok());

        // Invalid warmup counts
        assert!(validate_warmup_iterations(11).is_err());
        assert!(validate_warmup_iterations(100).is_err());
    }

    #[test]
    fn test_validate_memory_size() {
        // Valid memory sizes
        assert!(validate_memory_size(1024 * 1024, 1024 * 1024 * 1024).is_ok()); // 1MB
        assert!(validate_memory_size(100 * 1024 * 1024, 1024 * 1024 * 1024).is_ok()); // 100MB

        // Invalid memory sizes
        assert!(validate_memory_size(1024, 1024 * 1024 * 1024).is_err()); // Too small
        assert!(validate_memory_size(2 * 1024 * 1024 * 1024, 1024 * 1024 * 1024).is_err()); // Too large
    }

    #[test]
    fn test_validate_port() {
        // Valid ports
        assert!(validate_port(1).is_ok());
        assert!(validate_port(80).is_ok());
        assert!(validate_port(8080).is_ok());
        assert!(validate_port(65535).is_ok());

        // Invalid ports
        assert!(validate_port(0).is_err());
    }

    #[test]
    fn test_validate_disk_space() {
        let temp_dir = TempDir::new().unwrap();

        // Valid disk space (small requirement)
        assert!(validate_disk_space(temp_dir.path(), 1024 * 1024).is_ok()); // 1MB

        // Invalid disk space (unreasonably large requirement)
        let huge_requirement = 1024u64 * 1024 * 1024 * 1024 * 1024; // 1PB
        assert!(validate_disk_space(temp_dir.path(), huge_requirement).is_err());
    }

    #[test]
    fn test_is_valid_hostname() {
        // Valid hostnames
        assert!(is_valid_hostname("localhost"));
        assert!(is_valid_hostname("example.com"));
        assert!(is_valid_hostname("test-server"));
        assert!(is_valid_hostname("sub.domain.example.com"));

        // Invalid hostnames
        assert!(!is_valid_hostname(""));
        assert!(!is_valid_hostname("-invalid"));
        assert!(!is_valid_hostname("invalid-"));
        assert!(!is_valid_hostname("invalid..com"));
    }

    #[test]
    fn test_is_valid_ip() {
        // Valid IPs
        assert!(is_valid_ip("127.0.0.1"));
        assert!(is_valid_ip("192.168.1.1"));
        assert!(is_valid_ip("::1"));
        assert!(is_valid_ip("2001:db8::1"));

        // Invalid IPs
        assert!(!is_valid_ip("256.256.256.256"));
        assert!(!is_valid_ip("not.an.ip"));
        assert!(!is_valid_ip(""));
    }
}
