//! GPU-specific error types
//!
//! All GPU errors are recoverable — the caller can always fall back to CPU paths.

use std::fmt;

/// GPU-specific errors.
///
/// All variants are designed to be **non-fatal**: when a GPU error occurs,
/// the system should gracefully degrade to CPU execution rather than panicking.
#[derive(Debug)]
pub enum GpuError {
    /// CUDA driver not found or not accessible
    DriverNotFound(String),

    /// No compatible GPU device found
    NoDeviceFound(String),

    /// GPU device has insufficient compute capability
    InsufficientComputeCapability {
        device_name: String,
        found: (u32, u32),
        required: (u32, u32),
    },

    /// GPU out of memory
    OutOfMemory {
        requested_bytes: usize,
        available_bytes: usize,
        device_name: String,
    },

    /// CUDA kernel execution error
    KernelError(String),

    /// Memory transfer error (host ↔ device)
    TransferError(String),

    /// Device initialization failed
    InitializationError(String),

    /// Feature not supported on this GPU
    UnsupportedFeature(String),

    /// Generic GPU error with context
    Other(String),
}

impl fmt::Display for GpuError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            GpuError::DriverNotFound(msg) => {
                write!(f, "CUDA driver not found: {msg}")
            }
            GpuError::NoDeviceFound(msg) => {
                write!(f, "No compatible GPU device: {msg}")
            }
            GpuError::InsufficientComputeCapability {
                device_name,
                found,
                required,
            } => {
                write!(
                    f,
                    "GPU '{device_name}' compute capability {}.{} < required {}.{}",
                    found.0, found.1, required.0, required.1
                )
            }
            GpuError::OutOfMemory {
                requested_bytes,
                available_bytes,
                device_name,
            } => {
                let req_mb = *requested_bytes as f64 / (1024.0 * 1024.0);
                let avail_mb = *available_bytes as f64 / (1024.0 * 1024.0);
                write!(
                    f,
                    "GPU '{device_name}' OOM: requested {req_mb:.1}MB, available {avail_mb:.1}MB"
                )
            }
            GpuError::KernelError(msg) => {
                write!(f, "CUDA kernel error: {msg}")
            }
            GpuError::TransferError(msg) => {
                write!(f, "GPU memory transfer error: {msg}")
            }
            GpuError::InitializationError(msg) => {
                write!(f, "GPU initialization error: {msg}")
            }
            GpuError::UnsupportedFeature(msg) => {
                write!(f, "GPU feature not supported: {msg}")
            }
            GpuError::Other(msg) => {
                write!(f, "GPU error: {msg}")
            }
        }
    }
}

impl std::error::Error for GpuError {}

/// Result type for GPU operations.
pub type GpuResult<T> = Result<T, GpuError>;

impl GpuError {
    /// Whether this error means we should fall back to CPU.
    ///
    /// Returns `true` for all variants — GPU errors are always recoverable
    /// by falling back to the CPU path.
    pub fn should_fallback_to_cpu(&self) -> bool {
        true
    }

    /// Human-readable suggestion for the user.
    pub fn user_hint(&self) -> &str {
        match self {
            GpuError::DriverNotFound(_) => {
                "Install NVIDIA CUDA Toolkit 11.8+ from https://developer.nvidia.com/cuda-downloads"
            }
            GpuError::NoDeviceFound(_) => {
                "No NVIDIA GPU detected. GPU acceleration requires an NVIDIA GPU with Pascal architecture or newer."
            }
            GpuError::InsufficientComputeCapability { .. } => {
                "Your GPU is too old. GPU index building requires NVIDIA Pascal (GTX 10xx) or newer."
            }
            GpuError::OutOfMemory { .. } => {
                "Try reducing batch size or using Hybrid mode (cpu_threads + smaller gpu_batch_size)."
            }
            GpuError::KernelError(_) | GpuError::TransferError(_) => {
                "This may be a transient error. Try again or update your NVIDIA drivers."
            }
            GpuError::InitializationError(_) => {
                "Check that CUDA Toolkit is properly installed and your GPU drivers are up to date."
            }
            GpuError::UnsupportedFeature(_) => {
                "This feature requires a newer GPU or CUDA Toolkit version."
            }
            GpuError::Other(_) => {
                "An unexpected GPU error occurred. The system will fall back to CPU automatically."
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_gpu_error_display() {
        let err = GpuError::OutOfMemory {
            requested_bytes: 1024 * 1024 * 512,
            available_bytes: 1024 * 1024 * 256,
            device_name: "RTX 4090".into(),
        };
        let msg = format!("{err}");
        assert!(msg.contains("RTX 4090"));
        assert!(msg.contains("512.0MB"));
        assert!(msg.contains("256.0MB"));
    }

    #[test]
    fn test_all_errors_should_fallback() {
        let errors = vec![
            GpuError::DriverNotFound("test".into()),
            GpuError::NoDeviceFound("test".into()),
            GpuError::InsufficientComputeCapability {
                device_name: "test".into(),
                found: (5, 0),
                required: (6, 0),
            },
            GpuError::OutOfMemory {
                requested_bytes: 100,
                available_bytes: 50,
                device_name: "test".into(),
            },
            GpuError::KernelError("test".into()),
            GpuError::TransferError("test".into()),
            GpuError::InitializationError("test".into()),
            GpuError::UnsupportedFeature("test".into()),
            GpuError::Other("test".into()),
        ];

        for err in &errors {
            assert!(
                err.should_fallback_to_cpu(),
                "Error {:?} should allow CPU fallback",
                err
            );
            assert!(
                !err.user_hint().is_empty(),
                "Error {:?} should have a user hint",
                err
            );
        }
    }
}
