//! This module is intentionally independent from the rest of the crate so it
//! can be adopted incrementally by modules that currently panic on invalid
//! input.

use std::fmt;

pub type TuphiumResult<T> = Result<T, TuphiumError>;

#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum TuphiumError {
    ShapeMismatch {
        operation: &'static str,
        expected: Vec<usize>,
        actual: Vec<usize>,
    },

    LengthMismatch {
        operation: &'static str,
        expected: usize,
        actual: usize,
    },

    IndexOutOfBounds {
        operation: &'static str,
        row: usize,
        col: usize,
        rows: usize,
        cols: usize,
    },

    RowIndexOutOfBounds {
        operation: &'static str,
        row: usize,
        rows: usize,
    },

    InvalidDimension {
        operation: &'static str,
        dimension: &'static str,
        value: usize,
        reason: &'static str,
    },

    DimensionOverflow {
        operation: &'static str,
        rows: usize,
        cols: usize,
    },

    InvalidState {
        operation: &'static str,
        state: &'static str,
    },

    DivisionByZero {
        operation: &'static str,
        numerator: f32,
    },

    NumericDomain {
        operation: &'static str,
        input: f32,
        reason: &'static str,
    },

    NonFiniteValue {
        operation: &'static str,
        value: f32,
    },

    IoError {
        operation: &'static str,
        path: Option<String>,
        message: String,
    },

    InvalidFileExtension {
        operation: &'static str,
        path: String,
        expected_extension: String,
    },

    EmptyData {
        operation: &'static str,
        target: String,
    },

    MalformedCsv {
        operation: &'static str,
        row: usize,
        expected_fields: usize,
        actual_fields: usize,
    },

    ColumnNotFound {
        operation: &'static str,
        column: String,
    },

    ColumnTypeMismatch {
        operation: &'static str,
        column: String,
        expected: String,
        actual: String,
    },

    NoNumericColumns {
        operation: &'static str,
    },

    InvalidSplitFractions {
        operation: &'static str,
        train: f32,
        validation: f32,
        test: f32,
        sum: f32,
    },

    InvalidTargetShape {
        operation: &'static str,
        expected_columns: usize,
        actual_columns: usize,
    },

    BackendUnavailable {
        operation: &'static str,
        backend: String,
        message: String,
    },

    BackendOperationFailed {
        operation: &'static str,
        backend: String,
        message: String,
    },
}

impl fmt::Display for TuphiumError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::ShapeMismatch {
                operation,
                expected,
                actual,
            } => write!(
                formatter,
                "{operation}: shape mismatch (expected {expected:?}, got {actual:?})"
            ),
            Self::LengthMismatch {
                operation,
                expected,
                actual,
            } => write!(
                formatter,
                "{operation}: length mismatch (expected {expected}, got {actual})"
            ),
            Self::IndexOutOfBounds {
                operation,
                row,
                col,
                rows,
                cols,
            } => write!(
                formatter,
                "{operation}: index ({row}, {col}) is outside matrix bounds ({rows}, {cols})"
            ),
            Self::RowIndexOutOfBounds {
                operation,
                row,
                rows,
            } => write!(
                formatter,
                "{operation}: row index {row} is outside row bounds ({rows})"
            ),
            Self::InvalidDimension {
                operation,
                dimension,
                value,
                reason,
            } => write!(
                formatter,
                "{operation}: invalid {dimension} dimension {value} ({reason})"
            ),
            Self::DimensionOverflow {
                operation,
                rows,
                cols,
            } => write!(
                formatter,
                "{operation}: matrix dimensions ({rows}, {cols}) overflow the addressable size"
            ),
            Self::InvalidState { operation, state } => {
                write!(formatter, "{operation}: invalid state ({state})")
            }
            Self::DivisionByZero {
                operation,
                numerator,
            } => write!(
                formatter,
                "{operation}: division by zero with numerator {numerator}"
            ),
            Self::NumericDomain {
                operation,
                input,
                reason,
            } => write!(
                formatter,
                "{operation}: input {input} is outside the numeric domain ({reason})"
            ),
            Self::NonFiniteValue { operation, value } => {
                write!(
                    formatter,
                    "{operation}: encountered non-finite value {value}"
                )
            }
            Self::IoError {
                operation,
                path,
                message,
            } => match path {
                Some(path) => write!(formatter, "{operation}: I/O error for '{path}' ({message})"),
                None => write!(formatter, "{operation}: I/O error ({message})"),
            },
            Self::InvalidFileExtension {
                operation,
                path,
                expected_extension,
            } => write!(
                formatter,
                "{operation}: invalid file extension for '{path}' (expected {expected_extension})"
            ),
            Self::EmptyData { operation, target } => {
                write!(formatter, "{operation}: empty data ({target})")
            }
            Self::MalformedCsv {
                operation,
                row,
                expected_fields,
                actual_fields,
            } => write!(
                formatter,
                "{operation}: malformed CSV row {row} (expected {expected_fields} fields, got {actual_fields})"
            ),
            Self::ColumnNotFound { operation, column } => {
                write!(formatter, "{operation}: column '{column}' not found")
            }
            Self::ColumnTypeMismatch {
                operation,
                column,
                expected,
                actual,
            } => write!(
                formatter,
                "{operation}: column '{column}' has invalid type (expected {expected}, got {actual})"
            ),
            Self::NoNumericColumns { operation } => {
                write!(formatter, "{operation}: no numeric columns available")
            }
            Self::InvalidSplitFractions {
                operation,
                train,
                validation,
                test,
                sum,
            } => write!(
                formatter,
                "{operation}: invalid split fractions (train {train}, validation {validation}, test {test}, sum {sum})"
            ),
            Self::InvalidTargetShape {
                operation,
                expected_columns,
                actual_columns,
            } => write!(
                formatter,
                "{operation}: invalid target shape (expected {expected_columns} columns, got {actual_columns})"
            ),
            Self::BackendUnavailable {
                operation,
                backend,
                message,
            } => write!(
                formatter,
                "{operation}: backend '{backend}' is unavailable ({message})"
            ),
            Self::BackendOperationFailed {
                operation,
                backend,
                message,
            } => write!(
                formatter,
                "{operation}: backend '{backend}' operation failed ({message})"
            ),
        }
    }
}

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

#[cfg(test)]
mod tests {
    use super::{TuphiumError, TuphiumResult};
    use std::error::Error;

    #[test]
    fn display_describes_each_error_category() {
        let errors = [
            TuphiumError::ShapeMismatch {
                operation: "Matrix::matvec",
                expected: vec![2, 3],
                actual: vec![2, 2],
            },
            TuphiumError::LengthMismatch {
                operation: "Matrix::from_data",
                expected: 4,
                actual: 2,
            },
            TuphiumError::IndexOutOfBounds {
                operation: "Matrix::get",
                row: 2,
                col: 0,
                rows: 2,
                cols: 2,
            },
            TuphiumError::RowIndexOutOfBounds {
                operation: "DataFrame::select_rows",
                row: 3,
                rows: 2,
            },
            TuphiumError::InvalidDimension {
                operation: "Layer::new",
                dimension: "input",
                value: 0,
                reason: "must be greater than zero",
            },
            TuphiumError::DimensionOverflow {
                operation: "Matrix::zeros",
                rows: usize::MAX,
                cols: 2,
            },
            TuphiumError::InvalidState {
                operation: "Layer::backward",
                state: "forward must be called first",
            },
            TuphiumError::DivisionByZero {
                operation: "Value::div",
                numerator: 1.0,
            },
            TuphiumError::NumericDomain {
                operation: "Value::log",
                input: 0.0,
                reason: "logarithm requires a positive input",
            },
            TuphiumError::NonFiniteValue {
                operation: "Layer::forward",
                value: f32::INFINITY,
            },
            TuphiumError::IoError {
                operation: "read_csv",
                path: Some("data/missing.csv".to_string()),
                message: "file not found".to_string(),
            },
            TuphiumError::InvalidFileExtension {
                operation: "read_csv",
                path: "data/input.txt".to_string(),
                expected_extension: ".csv".to_string(),
            },
            TuphiumError::EmptyData {
                operation: "read_csv",
                target: "CSV file".to_string(),
            },
            TuphiumError::MalformedCsv {
                operation: "DataFrame::from_csv_data",
                row: 3,
                expected_fields: 4,
                actual_fields: 2,
            },
            TuphiumError::ColumnNotFound {
                operation: "DataFrame::select",
                column: "diagnosis".to_string(),
            },
            TuphiumError::ColumnTypeMismatch {
                operation: "DataFrame::to_matrix",
                column: "name".to_string(),
                expected: "numeric".to_string(),
                actual: "Text".to_string(),
            },
            TuphiumError::NoNumericColumns {
                operation: "DataFrame::to_matrix",
            },
            TuphiumError::InvalidSplitFractions {
                operation: "Dataset::split",
                train: 0.7,
                validation: 0.2,
                test: 0.2,
                sum: 1.1,
            },
            TuphiumError::InvalidTargetShape {
                operation: "Dataset::into_numeric",
                expected_columns: 1,
                actual_columns: 3,
            },
            TuphiumError::BackendUnavailable {
                operation: "Matrix::sync_to_device",
                backend: "CUDA".to_string(),
                message: "no CUDA-capable device found".to_string(),
            },
            TuphiumError::BackendOperationFailed {
                operation: "Matrix::matvec",
                backend: "cuBLAS".to_string(),
                message: "gemm failed".to_string(),
            },
        ];

        for error in errors {
            let message = error.to_string();
            assert!(!message.is_empty());
            assert!(message.contains(':'));
        }
    }

    #[test]
    fn errors_support_structural_comparison() {
        let first = TuphiumError::LengthMismatch {
            operation: "SGD::update",
            expected: 3,
            actual: 2,
        };
        let second = first.clone();

        assert_eq!(first, second);

        let first = TuphiumError::ColumnTypeMismatch {
            operation: "DataFrame::to_matrix",
            column: "name".to_string(),
            expected: "numeric".to_string(),
            actual: "Text".to_string(),
        };
        let second = first.clone();

        assert_eq!(first, second);
    }

    #[test]
    fn result_alias_carries_tuphium_errors() {
        fn validate_length() -> TuphiumResult<()> {
            Err(TuphiumError::LengthMismatch {
                operation: "test",
                expected: 2,
                actual: 1,
            })
        }

        assert!(matches!(
            validate_length(),
            Err(TuphiumError::LengthMismatch {
                expected: 2,
                actual: 1,
                ..
            })
        ));
    }

    #[test]
    fn tuphium_error_implements_standard_error() {
        let error = TuphiumError::InvalidState {
            operation: "Layer::backward",
            state: "missing cached forward pass",
        };
        let standard_error: &dyn Error = &error;

        assert_eq!(
            standard_error.to_string(),
            "Layer::backward: invalid state (missing cached forward pass)"
        );
        assert!(standard_error.source().is_none());
    }
}
