use thiserror::Error;

/// Main error type for expression evaluation
#[derive(Error, Debug)]
pub enum EvalError {
    /// Array index out of bounds
    #[error("Index {index} is out of bounds for array of length {length}")]
    IndexOutOfBounds { index: i64, length: usize },

    /// Feature not yet implemented
    #[error("Feature not implemented: {feature}")]
    NotImplemented { feature: String },

    /// Function has no evaluation implementation
    #[error("Function '{function_name}' has no evaluation implementation")]
    NoEvalImplementation { function_name: String },

    /// Variable not found in the evaluation environment
    #[error("Variable '{name}' not found in environment")]
    VariableNotFound { name: String },

    /// Error from function evaluation - wraps any error from the function registry
    #[error("Error in function '{function}': {error}")]
    FunctionError {
        function: String,
        #[source]
        error: Box<dyn std::error::Error + Send + Sync + 'static>,
    },

    /// General execution error - wraps anyhow errors and other runtime failures
    #[error("{message}")]
    ExecutionError {
        message: String,
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
    },
}

impl EvalError {
    /// Create a function error
    pub fn function_error(
        function: impl Into<String>,
        error: impl std::error::Error + Send + Sync + 'static,
    ) -> Self {
        Self::FunctionError {
            function: function.into(),
            error: Box::new(error),
        }
    }

    /// Create an execution error from a message
    pub fn execution(message: impl Into<String>) -> Self {
        Self::ExecutionError {
            message: message.into(),
            source: None,
        }
    }

    /// Create an execution error with a source error
    pub fn execution_with_source(
        message: impl Into<String>,
        source: impl std::error::Error + Send + Sync + 'static,
    ) -> Self {
        Self::ExecutionError {
            message: message.into(),
            source: Some(Box::new(source)),
        }
    }
}

impl From<anyhow::Error> for EvalError {
    fn from(err: anyhow::Error) -> Self {
        Self::ExecutionError {
            message: err.to_string(),
            source: None,
        }
    }
}

impl EvalError {
    /// Returns true if this error means the expression simply can't be evaluated
    /// at compile time (e.g., references a column, or uses a function without an
    /// eval implementation). These are expected during constant folding and the
    /// expression should be kept as-is.
    ///
    /// Returns false for genuine runtime errors like division by zero, index out
    /// of bounds, or type mismatches — these should be surfaced as hard failures.
    pub fn is_unfoldable(&self) -> bool {
        matches!(
            self,
            EvalError::NotImplemented { .. }
                | EvalError::NoEvalImplementation { .. }
                | EvalError::VariableNotFound { .. }
        )
    }
}

/// Result type for evaluation operations
pub type EvalResult<T> = Result<T, EvalError>;
