// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

use arrow::error::ArrowError;
use datafusion::common::DataFusionError;
use std::sync::Arc;

#[derive(thiserror::Error, Debug, Clone)]
pub enum SparkError {
    // This list was generated from the Spark code. Many of the exceptions are not yet used by Comet
    #[error("[CAST_INVALID_INPUT] The value '{value}' of the type \"{from_type}\" cannot be cast to \"{to_type}\" \
        because it is malformed. Correct the value as per the syntax, or change its target type. \
        Use `try_cast` to tolerate malformed input and return NULL instead. If necessary \
        set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error.")]
    CastInvalidValue {
        value: String,
        from_type: String,
        to_type: String,
    },

    /// Like CastInvalidValue but maps to SparkDateTimeException instead of SparkNumberFormatException.
    /// Used for string → timestamp/date cast failures.
    #[error("[CAST_INVALID_INPUT] The value '{value}' of the type \"{from_type}\" cannot be cast to \"{to_type}\" \
        because it is malformed. Correct the value as per the syntax, or change its target type. \
        Use `try_cast` to tolerate malformed input and return NULL instead. If necessary \
        set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error.")]
    InvalidInputInCastToDatetime {
        value: String,
        from_type: String,
        to_type: String,
    },

    #[error("[NUMERIC_VALUE_OUT_OF_RANGE.WITH_SUGGESTION] {value} cannot be represented as Decimal({precision}, {scale}). If necessary set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error, and return NULL instead.")]
    NumericValueOutOfRange {
        value: String,
        precision: u8,
        scale: i8,
    },

    #[error("[NUMERIC_OUT_OF_SUPPORTED_RANGE] The value {value} cannot be interpreted as a numeric since it has more than 38 digits.")]
    NumericOutOfRange { value: String },

    #[error("[CAST_OVERFLOW] The value {value} of the type \"{from_type}\" cannot be cast to \"{to_type}\" \
        due to an overflow. Use `try_cast` to tolerate overflow and return NULL instead. If necessary \
        set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error.")]
    CastOverFlow {
        value: String,
        from_type: String,
        to_type: String,
    },

    #[error("[CANNOT_PARSE_DECIMAL] Cannot parse decimal.")]
    CannotParseDecimal,

    #[error("[ARITHMETIC_OVERFLOW] {from_type} overflow. If necessary set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error.")]
    ArithmeticOverflow { from_type: String },

    #[error("[ARITHMETIC_OVERFLOW] Overflow in integral divide. Use `try_divide` to tolerate overflow and return NULL instead. If necessary set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error.")]
    IntegralDivideOverflow,

    #[error("[ARITHMETIC_OVERFLOW] Overflow in sum of decimals. Use `try_{function_name}` to tolerate overflow and return NULL instead. If necessary set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error.")]
    DecimalSumOverflow { function_name: String },

    #[error("[DIVIDE_BY_ZERO] Division by zero. Use `try_divide` to tolerate divisor being 0 and return NULL instead. If necessary set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error.")]
    DivideByZero,

    #[error("[REMAINDER_BY_ZERO] Remainder by zero. Use `try_mod` to tolerate divisor being 0 and return NULL instead. If necessary set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error.")]
    RemainderByZero,

    #[error("[INTERVAL_DIVIDED_BY_ZERO] Divide by zero in interval arithmetic.")]
    IntervalDividedByZero,

    #[error("[BINARY_ARITHMETIC_OVERFLOW] {value1} {symbol} {value2} caused overflow. Use `{function_name}` to tolerate overflow and return NULL instead. If necessary set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error.")]
    BinaryArithmeticOverflow {
        value1: String,
        symbol: String,
        value2: String,
        function_name: String,
    },

    #[error("[INTERVAL_ARITHMETIC_OVERFLOW.WITH_SUGGESTION] Interval arithmetic overflow. Use `{function_name}` to tolerate overflow and return NULL instead. If necessary set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error.")]
    IntervalArithmeticOverflowWithSuggestion { function_name: String },

    #[error("[INTERVAL_ARITHMETIC_OVERFLOW.WITHOUT_SUGGESTION] Interval arithmetic overflow. If necessary set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error.")]
    IntervalArithmeticOverflowWithoutSuggestion,

    #[error("[DATETIME_OVERFLOW] Datetime arithmetic overflow.")]
    DatetimeOverflow,

    #[error("[INVALID_ARRAY_INDEX] The index {index_value} is out of bounds. The array has {array_size} elements. Use the SQL function get() to tolerate accessing element at invalid index and return NULL instead. If necessary set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error.")]
    InvalidArrayIndex { index_value: i32, array_size: i32 },

    #[error("[INVALID_ARRAY_INDEX_IN_ELEMENT_AT] The index {index_value} is out of bounds. The array has {array_size} elements. Use try_element_at to tolerate accessing element at invalid index and return NULL instead. If necessary set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error.")]
    InvalidElementAtIndex { index_value: i32, array_size: i32 },

    #[error("[INVALID_BITMAP_POSITION] The bit position {bit_position} is out of bounds. The bitmap has {bitmap_num_bytes} bytes ({bitmap_num_bits} bits).")]
    InvalidBitmapPosition {
        bit_position: i64,
        bitmap_num_bytes: i64,
        bitmap_num_bits: i64,
    },

    #[error("[INVALID_INDEX_OF_ZERO] The index 0 is invalid. An index shall be either < 0 or > 0 (the first element has index 1).")]
    InvalidIndexOfZero,

    #[error("[DUPLICATED_MAP_KEY] Cannot create map with duplicate keys: {key}.")]
    DuplicatedMapKey { key: String },

    #[error("[NULL_MAP_KEY] Cannot use null as map key.")]
    NullMapKey,

    #[error("[MAP_KEY_VALUE_DIFF_SIZES] The key array and value array of a map must have the same length.")]
    MapKeyValueDiffSizes,

    #[error("[EXCEED_LIMIT_LENGTH] Cannot create a map with {size} elements which exceeds the limit {max_size}.")]
    ExceedMapSizeLimit { size: i32, max_size: i32 },

    #[error("[COLLECTION_SIZE_LIMIT_EXCEEDED] Cannot create array with {num_elements} elements which exceeds the limit {max_elements}.")]
    CollectionSizeLimitExceeded {
        num_elements: i64,
        max_elements: i64,
    },

    #[error("[NOT_NULL_ASSERT_VIOLATION] The field `{field_name}` cannot be null.")]
    NotNullAssertViolation { field_name: String },

    #[error("[VALUE_IS_NULL] The value of field `{field_name}` at row {row_index} is null.")]
    ValueIsNull { field_name: String, row_index: i32 },

    #[error("[CANNOT_PARSE_TIMESTAMP] Cannot parse timestamp: {message}. Try using `{suggested_func}` instead.")]
    CannotParseTimestamp {
        message: String,
        suggested_func: String,
    },

    #[error("[INVALID_FRACTION_OF_SECOND] The fraction of second {value} is invalid. Valid values are in the range [0, 60]. If necessary set \"spark.sql.ansi.enabled\" to \"false\" to bypass this error.")]
    InvalidFractionOfSecond { value: f64 },

    #[error("[INVALID_UTF8_STRING] Invalid UTF-8 string: {hex_string}.")]
    InvalidUtf8String { hex_string: String },

    #[error("[UNEXPECTED_POSITIVE_VALUE] The {parameter_name} parameter must be less than or equal to 0. The actual value is {actual_value}.")]
    UnexpectedPositiveValue {
        parameter_name: String,
        actual_value: i32,
    },

    #[error("[UNEXPECTED_NEGATIVE_VALUE] The {parameter_name} parameter must be greater than or equal to 0. The actual value is {actual_value}.")]
    UnexpectedNegativeValue {
        parameter_name: String,
        actual_value: i32,
    },

    #[error("[INVALID_PARAMETER_VALUE] Invalid regex group index {group_index} in function `{function_name}`. Group count is {group_count}.")]
    InvalidRegexGroupIndex {
        function_name: String,
        group_count: i32,
        group_index: i32,
    },

    #[error("[DATATYPE_CANNOT_ORDER] Cannot order by type: {data_type}.")]
    DatatypeCannotOrder { data_type: String },

    #[error("[SCALAR_SUBQUERY_TOO_MANY_ROWS] Scalar subquery returned more than one row.")]
    ScalarSubqueryTooManyRows,

    #[error("{message}")]
    FileNotFound { message: String },

    #[error("[_LEGACY_ERROR_TEMP_2093] Found duplicate field(s) \"{required_field_name}\": [{matched_fields}] in case-insensitive mode")]
    DuplicateFieldCaseInsensitive {
        required_field_name: String,
        matched_fields: String,
    },

    /// Multiple Parquet fields share the same field id when the read schema requested an
    /// id-based lookup. Mirrors Spark's `_LEGACY_ERROR_TEMP_2094`
    /// (`foundDuplicateFieldInFieldIdLookupModeError`).
    #[error("[_LEGACY_ERROR_TEMP_2094] Found duplicate field(s) by id: id={required_id} matches [{matched_fields}] in id-lookup mode")]
    DuplicateFieldByFieldId {
        required_id: i32,
        matched_fields: String,
    },

    /// The read schema requests Parquet field-id matching but the file carries no field ids.
    /// Mirrors the runtime error raised in Spark's `ParquetReadSupport` when
    /// `spark.sql.parquet.fieldId.read.ignoreMissing` is false.
    #[error("Spark read schema expects field Ids, but Parquet file schema doesn't contain any field Ids. Please remove the field ids from Spark schema or ignore missing ids by setting `spark.sql.parquet.fieldId.read.ignoreMissing = true`")]
    ParquetMissingFieldIds,

    /// Schema mismatch when reading a Parquet column under a requested schema
    /// that's incompatible with the physical column type. Translated by the JVM
    /// shim into Spark's `SchemaColumnConvertNotSupportedException`. The
    /// `file_path` may be empty when the rejection happens at planning time
    /// (the schema adapter doesn't carry the file path).
    #[error("Parquet column cannot be converted in file {file_path}. Column: [{column}], Expected: {spark_type}, Found: {physical_type}")]
    ParquetSchemaConvert {
        file_path: String,
        column: String,
        physical_type: String,
        spark_type: String,
    },

    #[error("ArrowError: {0}.")]
    Arrow(Arc<ArrowError>),

    #[error("InternalError: {0}.")]
    Internal(String),
}

impl SparkError {
    /// Serialize this error to JSON format for JNI transfer
    pub fn to_json(&self) -> String {
        let error_class = self.error_class().unwrap_or("");

        // Create a JSON structure with errorType, errorClass, and params
        match serde_json::to_string(&serde_json::json!({
            "errorType": self.error_type_name(),
            "errorClass": error_class,
            "params": self.params_as_json(),
        })) {
            Ok(json) => json,
            Err(e) => {
                // Fallback if serialization fails
                format!(
                    "{{\"errorType\":\"SerializationError\",\"message\":\"{}\"}}",
                    e
                )
            }
        }
    }

    /// Get the error type name for JSON serialization
    pub(crate) fn error_type_name(&self) -> &'static str {
        match self {
            SparkError::CastInvalidValue { .. } => "CastInvalidValue",
            SparkError::InvalidInputInCastToDatetime { .. } => "InvalidInputInCastToDatetime",
            SparkError::NumericValueOutOfRange { .. } => "NumericValueOutOfRange",
            SparkError::NumericOutOfRange { .. } => "NumericOutOfRange",
            SparkError::CastOverFlow { .. } => "CastOverFlow",
            SparkError::CannotParseDecimal => "CannotParseDecimal",
            SparkError::ArithmeticOverflow { .. } => "ArithmeticOverflow",
            SparkError::IntegralDivideOverflow => "IntegralDivideOverflow",
            SparkError::DecimalSumOverflow { .. } => "DecimalSumOverflow",
            SparkError::DivideByZero => "DivideByZero",
            SparkError::RemainderByZero => "RemainderByZero",
            SparkError::IntervalDividedByZero => "IntervalDividedByZero",
            SparkError::BinaryArithmeticOverflow { .. } => "BinaryArithmeticOverflow",
            SparkError::IntervalArithmeticOverflowWithSuggestion { .. } => {
                "IntervalArithmeticOverflowWithSuggestion"
            }
            SparkError::IntervalArithmeticOverflowWithoutSuggestion => {
                "IntervalArithmeticOverflowWithoutSuggestion"
            }
            SparkError::DatetimeOverflow => "DatetimeOverflow",
            SparkError::InvalidArrayIndex { .. } => "InvalidArrayIndex",
            SparkError::InvalidElementAtIndex { .. } => "InvalidElementAtIndex",
            SparkError::InvalidBitmapPosition { .. } => "InvalidBitmapPosition",
            SparkError::InvalidIndexOfZero => "InvalidIndexOfZero",
            SparkError::DuplicatedMapKey { .. } => "DuplicatedMapKey",
            SparkError::NullMapKey => "NullMapKey",
            SparkError::MapKeyValueDiffSizes => "MapKeyValueDiffSizes",
            SparkError::ExceedMapSizeLimit { .. } => "ExceedMapSizeLimit",
            SparkError::CollectionSizeLimitExceeded { .. } => "CollectionSizeLimitExceeded",
            SparkError::NotNullAssertViolation { .. } => "NotNullAssertViolation",
            SparkError::ValueIsNull { .. } => "ValueIsNull",
            SparkError::CannotParseTimestamp { .. } => "CannotParseTimestamp",
            SparkError::InvalidFractionOfSecond { .. } => "InvalidFractionOfSecond",
            SparkError::InvalidUtf8String { .. } => "InvalidUtf8String",
            SparkError::UnexpectedPositiveValue { .. } => "UnexpectedPositiveValue",
            SparkError::UnexpectedNegativeValue { .. } => "UnexpectedNegativeValue",
            SparkError::InvalidRegexGroupIndex { .. } => "InvalidRegexGroupIndex",
            SparkError::DatatypeCannotOrder { .. } => "DatatypeCannotOrder",
            SparkError::ScalarSubqueryTooManyRows => "ScalarSubqueryTooManyRows",
            SparkError::FileNotFound { .. } => "FileNotFound",
            SparkError::DuplicateFieldCaseInsensitive { .. } => "DuplicateFieldCaseInsensitive",
            SparkError::DuplicateFieldByFieldId { .. } => "DuplicateFieldByFieldId",
            SparkError::ParquetMissingFieldIds => "ParquetMissingFieldIds",
            SparkError::ParquetSchemaConvert { .. } => "ParquetSchemaConvert",
            SparkError::Arrow(_) => "Arrow",
            SparkError::Internal(_) => "Internal",
        }
    }

    /// Extract parameters as JSON value
    pub(crate) fn params_as_json(&self) -> serde_json::Value {
        match self {
            SparkError::CastInvalidValue {
                value,
                from_type,
                to_type,
            } => {
                serde_json::json!({
                    "value": value,
                    "fromType": from_type,
                    "toType": to_type,
                })
            }
            SparkError::InvalidInputInCastToDatetime {
                value,
                from_type,
                to_type,
            } => {
                serde_json::json!({
                    "value": value,
                    "fromType": from_type,
                    "toType": to_type,
                })
            }
            SparkError::NumericValueOutOfRange {
                value,
                precision,
                scale,
            } => {
                serde_json::json!({
                    "value": value,
                    "precision": precision,
                    "scale": scale,
                })
            }
            SparkError::NumericOutOfRange { value } => {
                serde_json::json!({
                    "value": value,
                })
            }
            SparkError::CastOverFlow {
                value,
                from_type,
                to_type,
            } => {
                serde_json::json!({
                    "value": value,
                    "fromType": from_type,
                    "toType": to_type,
                })
            }
            SparkError::ArithmeticOverflow { from_type } => {
                serde_json::json!({
                    "fromType": from_type,
                })
            }
            SparkError::DecimalSumOverflow { function_name } => {
                serde_json::json!({
                    "functionName": function_name,
                })
            }
            SparkError::BinaryArithmeticOverflow {
                value1,
                symbol,
                value2,
                function_name,
            } => {
                serde_json::json!({
                    "value1": value1,
                    "symbol": symbol,
                    "value2": value2,
                    "functionName": function_name,
                })
            }
            SparkError::IntervalArithmeticOverflowWithSuggestion { function_name } => {
                serde_json::json!({
                    "functionName": function_name,
                })
            }
            SparkError::InvalidArrayIndex {
                index_value,
                array_size,
            } => {
                serde_json::json!({
                    "indexValue": index_value,
                    "arraySize": array_size,
                })
            }
            SparkError::InvalidElementAtIndex {
                index_value,
                array_size,
            } => {
                serde_json::json!({
                    "indexValue": index_value,
                    "arraySize": array_size,
                })
            }
            SparkError::InvalidBitmapPosition {
                bit_position,
                bitmap_num_bytes,
                bitmap_num_bits,
            } => {
                serde_json::json!({
                    "bitPosition": bit_position,
                    "bitmapNumBytes": bitmap_num_bytes,
                    "bitmapNumBits": bitmap_num_bits,
                })
            }
            SparkError::DuplicatedMapKey { key } => {
                serde_json::json!({
                    "key": key,
                })
            }
            SparkError::ExceedMapSizeLimit { size, max_size } => {
                serde_json::json!({
                    "size": size,
                    "maxSize": max_size,
                })
            }
            SparkError::CollectionSizeLimitExceeded {
                num_elements,
                max_elements,
            } => {
                serde_json::json!({
                    "numElements": num_elements,
                    "maxElements": max_elements,
                })
            }
            SparkError::NotNullAssertViolation { field_name } => {
                serde_json::json!({
                    "fieldName": field_name,
                })
            }
            SparkError::ValueIsNull {
                field_name,
                row_index,
            } => {
                serde_json::json!({
                    "fieldName": field_name,
                    "rowIndex": row_index,
                })
            }
            SparkError::CannotParseTimestamp {
                message,
                suggested_func,
            } => {
                serde_json::json!({
                    "message": message,
                    "suggestedFunc": suggested_func,
                })
            }
            SparkError::InvalidFractionOfSecond { value } => {
                serde_json::json!({
                    "value": value,
                })
            }
            SparkError::InvalidUtf8String { hex_string } => {
                serde_json::json!({
                    "hexString": hex_string,
                })
            }
            SparkError::UnexpectedPositiveValue {
                parameter_name,
                actual_value,
            } => {
                serde_json::json!({
                    "parameterName": parameter_name,
                    "actualValue": actual_value,
                })
            }
            SparkError::UnexpectedNegativeValue {
                parameter_name,
                actual_value,
            } => {
                serde_json::json!({
                    "parameterName": parameter_name,
                    "actualValue": actual_value,
                })
            }
            SparkError::InvalidRegexGroupIndex {
                function_name,
                group_count,
                group_index,
            } => {
                serde_json::json!({
                    "functionName": function_name,
                    "groupCount": group_count,
                    "groupIndex": group_index,
                })
            }
            SparkError::DatatypeCannotOrder { data_type } => {
                serde_json::json!({
                    "dataType": data_type,
                })
            }
            SparkError::FileNotFound { message } => {
                serde_json::json!({
                    "message": message,
                })
            }
            SparkError::DuplicateFieldCaseInsensitive {
                required_field_name,
                matched_fields,
            } => {
                serde_json::json!({
                    "requiredFieldName": required_field_name,
                    "matchedOrcFields": matched_fields,
                })
            }
            SparkError::DuplicateFieldByFieldId {
                required_id,
                matched_fields,
            } => {
                serde_json::json!({
                    "requiredId": required_id,
                    "matchedFields": matched_fields,
                })
            }
            SparkError::ParquetSchemaConvert {
                file_path,
                column,
                physical_type,
                spark_type,
            } => {
                serde_json::json!({
                    "filePath": file_path,
                    "column": column,
                    "physicalType": physical_type,
                    "sparkType": spark_type,
                })
            }
            SparkError::Arrow(e) => {
                serde_json::json!({
                    "message": e.to_string(),
                })
            }
            SparkError::Internal(msg) => {
                serde_json::json!({
                    "message": msg,
                })
            }
            // Simple errors with no parameters
            _ => serde_json::json!({}),
        }
    }

    /// Returns the appropriate Spark exception class for this error
    pub fn exception_class(&self) -> &'static str {
        match self {
            // ArithmeticException
            SparkError::DivideByZero
            | SparkError::RemainderByZero
            | SparkError::IntervalDividedByZero
            | SparkError::NumericValueOutOfRange { .. }
            | SparkError::NumericOutOfRange { .. } // Comet-specific extension
            | SparkError::ArithmeticOverflow { .. }
            | SparkError::IntegralDivideOverflow
            | SparkError::DecimalSumOverflow { .. }
            | SparkError::BinaryArithmeticOverflow { .. }
            | SparkError::IntervalArithmeticOverflowWithSuggestion { .. }
            | SparkError::IntervalArithmeticOverflowWithoutSuggestion
            | SparkError::DatetimeOverflow => "org/apache/spark/SparkArithmeticException",

            // CastOverflow gets special handling with CastOverflowException
            SparkError::CastOverFlow { .. } => "org/apache/spark/sql/comet/CastOverflowException",

            // NumberFormatException (for cast invalid input errors)
            SparkError::CastInvalidValue { .. } => "org/apache/spark/SparkNumberFormatException",

            // ArrayIndexOutOfBoundsException
            SparkError::InvalidArrayIndex { .. }
            | SparkError::InvalidElementAtIndex { .. }
            | SparkError::InvalidBitmapPosition { .. }
            | SparkError::InvalidIndexOfZero => "org/apache/spark/SparkArrayIndexOutOfBoundsException",

            // RuntimeException
            SparkError::CannotParseDecimal
            | SparkError::DuplicatedMapKey { .. }
            | SparkError::NullMapKey
            | SparkError::MapKeyValueDiffSizes
            | SparkError::ExceedMapSizeLimit { .. }
            | SparkError::CollectionSizeLimitExceeded { .. }
            | SparkError::NotNullAssertViolation { .. }
            | SparkError::ValueIsNull { .. } // Comet-specific extension
            | SparkError::UnexpectedPositiveValue { .. }
            | SparkError::UnexpectedNegativeValue { .. }
            | SparkError::InvalidRegexGroupIndex { .. }
            | SparkError::ScalarSubqueryTooManyRows => "org/apache/spark/SparkRuntimeException",

            // DateTimeException
            SparkError::InvalidInputInCastToDatetime { .. }
            | SparkError::CannotParseTimestamp { .. }
            | SparkError::InvalidFractionOfSecond { .. } => "org/apache/spark/SparkDateTimeException",

            // IllegalArgumentException
            SparkError::DatatypeCannotOrder { .. }
            | SparkError::InvalidUtf8String { .. } => "org/apache/spark/SparkIllegalArgumentException",

            // FileNotFound - will be converted to SparkFileNotFoundException by the shim
            SparkError::FileNotFound { .. } => "org/apache/spark/SparkException",

            // DuplicateFieldCaseInsensitive - converted to SparkRuntimeException by the shim
            SparkError::DuplicateFieldCaseInsensitive { .. } => {
                "org/apache/spark/SparkRuntimeException"
            }

            // DuplicateFieldByFieldId - converted to SparkRuntimeException by the shim
            // (Spark's `foundDuplicateFieldInFieldIdLookupModeError` returns SparkRuntimeException)
            SparkError::DuplicateFieldByFieldId { .. } => "org/apache/spark/SparkRuntimeException",

            // ParquetMissingFieldIds - converted to a plain RuntimeException by the shim,
            // matching the `RuntimeException` Spark's ParquetReadSupport throws when the
            // file lacks field ids and `spark.sql.parquet.fieldId.read.ignoreMissing=false`.
            SparkError::ParquetMissingFieldIds => "java/lang/RuntimeException",

            // ParquetSchemaConvert - converted to SchemaColumnConvertNotSupportedException by the shim
            SparkError::ParquetSchemaConvert { .. } => {
                "org/apache/spark/sql/execution/datasources/SchemaColumnConvertNotSupportedException"
            }

            // Generic errors
            SparkError::Arrow(_) | SparkError::Internal(_) => "org/apache/spark/SparkException",
        }
    }

    /// Returns the Spark error class code for this error
    pub(crate) fn error_class(&self) -> Option<&'static str> {
        match self {
            // Cast errors
            SparkError::CastInvalidValue { .. } => Some("CAST_INVALID_INPUT"),
            SparkError::InvalidInputInCastToDatetime { .. } => Some("CAST_INVALID_INPUT"),
            SparkError::CastOverFlow { .. } => Some("CAST_OVERFLOW"),
            SparkError::NumericValueOutOfRange { .. } => {
                Some("NUMERIC_VALUE_OUT_OF_RANGE.WITH_SUGGESTION")
            }
            SparkError::NumericOutOfRange { .. } => Some("NUMERIC_OUT_OF_SUPPORTED_RANGE"),
            SparkError::CannotParseDecimal => Some("CANNOT_PARSE_DECIMAL"),

            // Arithmetic errors
            SparkError::DivideByZero => Some("DIVIDE_BY_ZERO"),
            SparkError::RemainderByZero => Some("REMAINDER_BY_ZERO"),
            SparkError::IntervalDividedByZero => Some("INTERVAL_DIVIDED_BY_ZERO"),
            SparkError::ArithmeticOverflow { .. } => Some("ARITHMETIC_OVERFLOW"),
            SparkError::IntegralDivideOverflow => Some("ARITHMETIC_OVERFLOW"),
            SparkError::DecimalSumOverflow { .. } => Some("ARITHMETIC_OVERFLOW"),
            SparkError::BinaryArithmeticOverflow { .. } => Some("BINARY_ARITHMETIC_OVERFLOW"),
            SparkError::IntervalArithmeticOverflowWithSuggestion { .. } => {
                Some("INTERVAL_ARITHMETIC_OVERFLOW")
            }
            SparkError::IntervalArithmeticOverflowWithoutSuggestion => {
                Some("INTERVAL_ARITHMETIC_OVERFLOW")
            }
            SparkError::DatetimeOverflow => Some("DATETIME_OVERFLOW"),

            // Array index errors
            SparkError::InvalidArrayIndex { .. } => Some("INVALID_ARRAY_INDEX"),
            SparkError::InvalidElementAtIndex { .. } => Some("INVALID_ARRAY_INDEX_IN_ELEMENT_AT"),
            SparkError::InvalidBitmapPosition { .. } => Some("INVALID_BITMAP_POSITION"),
            SparkError::InvalidIndexOfZero => Some("INVALID_INDEX_OF_ZERO"),

            // Map/Collection errors
            SparkError::DuplicatedMapKey { .. } => Some("DUPLICATED_MAP_KEY"),
            SparkError::NullMapKey => Some("NULL_MAP_KEY"),
            SparkError::MapKeyValueDiffSizes => Some("MAP_KEY_VALUE_DIFF_SIZES"),
            SparkError::ExceedMapSizeLimit { .. } => Some("EXCEED_LIMIT_LENGTH"),
            SparkError::CollectionSizeLimitExceeded { .. } => {
                Some("COLLECTION_SIZE_LIMIT_EXCEEDED")
            }

            // Null validation errors
            SparkError::NotNullAssertViolation { .. } => Some("NOT_NULL_ASSERT_VIOLATION"),
            SparkError::ValueIsNull { .. } => Some("VALUE_IS_NULL"),

            // DateTime errors
            SparkError::CannotParseTimestamp { .. } => Some("CANNOT_PARSE_TIMESTAMP"),
            SparkError::InvalidFractionOfSecond { .. } => Some("INVALID_FRACTION_OF_SECOND"),

            // String/UTF8 errors
            SparkError::InvalidUtf8String { .. } => Some("INVALID_UTF8_STRING"),

            // Function parameter errors
            SparkError::UnexpectedPositiveValue { .. } => Some("UNEXPECTED_POSITIVE_VALUE"),
            SparkError::UnexpectedNegativeValue { .. } => Some("UNEXPECTED_NEGATIVE_VALUE"),

            // Regex errors
            SparkError::InvalidRegexGroupIndex { .. } => Some("INVALID_PARAMETER_VALUE"),

            // Unsupported operation errors
            SparkError::DatatypeCannotOrder { .. } => Some("DATATYPE_CANNOT_ORDER"),

            // Subquery errors
            SparkError::ScalarSubqueryTooManyRows => Some("SCALAR_SUBQUERY_TOO_MANY_ROWS"),

            // File not found
            SparkError::FileNotFound { .. } => Some("_LEGACY_ERROR_TEMP_2055"),

            // Duplicate field in case-insensitive mode
            SparkError::DuplicateFieldCaseInsensitive { .. } => Some("_LEGACY_ERROR_TEMP_2093"),

            // Duplicate field id in id-lookup mode
            SparkError::DuplicateFieldByFieldId { .. } => Some("_LEGACY_ERROR_TEMP_2094"),

            // ParquetMissingFieldIds is a plain RuntimeException with no error class.
            SparkError::ParquetMissingFieldIds => None,

            // Parquet schema mismatch — translated to SchemaColumnConvertNotSupportedException
            // by the JVM shim. The shim wraps it in the version-appropriate
            // SparkException error class, so no error class is exposed here.
            SparkError::ParquetSchemaConvert { .. } => None,

            // Generic errors (no error class)
            SparkError::Arrow(_) | SparkError::Internal(_) => None,
        }
    }
}

pub type SparkResult<T> = Result<T, SparkError>;

/// Convert decimal overflow to SparkError::NumericValueOutOfRange.
pub fn decimal_overflow_error(value: i128, precision: u8, scale: i8) -> SparkError {
    SparkError::NumericValueOutOfRange {
        value: value.to_string(),
        precision,
        scale,
    }
}

/// Wrapper that adds QueryContext to SparkError
///
/// This allows attaching SQL context information (query text, line/position, object name) to errors
#[derive(Debug, Clone)]
pub struct SparkErrorWithContext {
    /// The underlying SparkError
    pub error: SparkError,
    /// Optional QueryContext for SQL location information
    pub context: Option<Arc<crate::QueryContext>>,
}

impl SparkErrorWithContext {
    /// Create a SparkErrorWithContext without context
    pub fn new(error: SparkError) -> Self {
        Self {
            error,
            context: None,
        }
    }

    /// Create a SparkErrorWithContext with QueryContext
    pub fn with_context(error: SparkError, context: Arc<crate::QueryContext>) -> Self {
        Self {
            error,
            context: Some(context),
        }
    }

    /// Serialize to JSON including optional context field
    pub fn to_json(&self) -> String {
        let mut json_obj = serde_json::json!({
            "errorType": self.error.error_type_name(),
            "errorClass": self.error.error_class().unwrap_or(""),
            "params": self.error.params_as_json(),
        });

        if let Some(ctx) = &self.context {
            // Serialize context fields
            json_obj["context"] = serde_json::json!({
                "sqlText": ctx.sql_text.as_str(),
                "startIndex": ctx.start_index,
                "stopIndex": ctx.stop_index,
                "objectType": ctx.object_type,
                "objectName": ctx.object_name,
                "line": ctx.line,
                "startPosition": ctx.start_position,
            });

            // Add formatted summary
            json_obj["summary"] = serde_json::json!(ctx.format_summary());
        }

        serde_json::to_string(&json_obj).unwrap_or_else(|e| {
            format!(
                "{{\"errorType\":\"SerializationError\",\"message\":\"{}\"}}",
                e
            )
        })
    }
}

impl std::fmt::Display for SparkErrorWithContext {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.error)?;
        if let Some(ctx) = &self.context {
            write!(f, "\n{}", ctx.format_summary())?;
        }
        Ok(())
    }
}

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

impl From<SparkError> for SparkErrorWithContext {
    fn from(error: SparkError) -> Self {
        SparkErrorWithContext::new(error)
    }
}

impl From<SparkErrorWithContext> for DataFusionError {
    fn from(value: SparkErrorWithContext) -> Self {
        DataFusionError::External(Box::new(value))
    }
}

impl From<ArrowError> for SparkError {
    fn from(value: ArrowError) -> Self {
        SparkError::Arrow(Arc::new(value))
    }
}

impl From<SparkError> for DataFusionError {
    fn from(value: SparkError) -> Self {
        DataFusionError::External(Box::new(value))
    }
}

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

    #[test]
    fn test_divide_by_zero_json() {
        let error = SparkError::DivideByZero;
        let json = error.to_json();

        assert!(json.contains("\"errorType\":\"DivideByZero\""));
        assert!(json.contains("\"errorClass\":\"DIVIDE_BY_ZERO\""));

        // Verify it's valid JSON
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["errorType"], "DivideByZero");
        assert_eq!(parsed["errorClass"], "DIVIDE_BY_ZERO");
    }

    #[test]
    fn test_remainder_by_zero_json() {
        let error = SparkError::RemainderByZero;
        let json = error.to_json();

        assert!(json.contains("\"errorType\":\"RemainderByZero\""));
        assert!(json.contains("\"errorClass\":\"REMAINDER_BY_ZERO\""));
    }

    #[test]
    fn test_binary_overflow_json() {
        let error = SparkError::BinaryArithmeticOverflow {
            value1: "32767".to_string(),
            symbol: "+".to_string(),
            value2: "1".to_string(),
            function_name: "try_add".to_string(),
        };
        let json = error.to_json();

        // Verify it's valid JSON
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["errorType"], "BinaryArithmeticOverflow");
        assert_eq!(parsed["errorClass"], "BINARY_ARITHMETIC_OVERFLOW");
        assert_eq!(parsed["params"]["value1"], "32767");
        assert_eq!(parsed["params"]["symbol"], "+");
        assert_eq!(parsed["params"]["value2"], "1");
        assert_eq!(parsed["params"]["functionName"], "try_add");
    }

    #[test]
    fn test_invalid_array_index_json() {
        let error = SparkError::InvalidArrayIndex {
            index_value: 10,
            array_size: 3,
        };
        let json = error.to_json();

        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["errorType"], "InvalidArrayIndex");
        assert_eq!(parsed["errorClass"], "INVALID_ARRAY_INDEX");
        assert_eq!(parsed["params"]["indexValue"], 10);
        assert_eq!(parsed["params"]["arraySize"], 3);
    }

    #[test]
    fn test_numeric_value_out_of_range_json() {
        let error = SparkError::NumericValueOutOfRange {
            value: "999.99".to_string(),
            precision: 5,
            scale: 2,
        };
        let json = error.to_json();

        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["errorType"], "NumericValueOutOfRange");
        assert_eq!(
            parsed["errorClass"],
            "NUMERIC_VALUE_OUT_OF_RANGE.WITH_SUGGESTION"
        );
        assert_eq!(parsed["params"]["value"], "999.99");
        assert_eq!(parsed["params"]["precision"], 5);
        assert_eq!(parsed["params"]["scale"], 2);
    }

    #[test]
    fn test_cast_invalid_value_json() {
        let error = SparkError::CastInvalidValue {
            value: "abc".to_string(),
            from_type: "STRING".to_string(),
            to_type: "INT".to_string(),
        };
        let json = error.to_json();

        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["errorType"], "CastInvalidValue");
        assert_eq!(parsed["errorClass"], "CAST_INVALID_INPUT");
        assert_eq!(parsed["params"]["value"], "abc");
        assert_eq!(parsed["params"]["fromType"], "STRING");
        assert_eq!(parsed["params"]["toType"], "INT");
    }

    #[test]
    fn test_duplicated_map_key_json() {
        let error = SparkError::DuplicatedMapKey {
            key: "duplicate_key".to_string(),
        };
        let json = error.to_json();

        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["errorType"], "DuplicatedMapKey");
        assert_eq!(parsed["errorClass"], "DUPLICATED_MAP_KEY");
        assert_eq!(parsed["params"]["key"], "duplicate_key");
    }

    #[test]
    fn test_null_map_key_json() {
        let error = SparkError::NullMapKey;
        let json = error.to_json();

        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["errorType"], "NullMapKey");
        assert_eq!(parsed["errorClass"], "NULL_MAP_KEY");
        // Params should be an empty object
        assert_eq!(parsed["params"], serde_json::json!({}));
    }

    #[test]
    fn test_error_class_mapping() {
        // Test that error_class() returns the correct error class
        assert_eq!(
            SparkError::DivideByZero.error_class(),
            Some("DIVIDE_BY_ZERO")
        );
        assert_eq!(
            SparkError::RemainderByZero.error_class(),
            Some("REMAINDER_BY_ZERO")
        );
        assert_eq!(
            SparkError::InvalidArrayIndex {
                index_value: 0,
                array_size: 0
            }
            .error_class(),
            Some("INVALID_ARRAY_INDEX")
        );
        assert_eq!(SparkError::NullMapKey.error_class(), Some("NULL_MAP_KEY"));
    }

    #[test]
    fn test_exception_class_mapping() {
        // Test that exception_class() returns the correct Java exception class
        assert_eq!(
            SparkError::DivideByZero.exception_class(),
            "org/apache/spark/SparkArithmeticException"
        );
        assert_eq!(
            SparkError::InvalidArrayIndex {
                index_value: 0,
                array_size: 0
            }
            .exception_class(),
            "org/apache/spark/SparkArrayIndexOutOfBoundsException"
        );
        assert_eq!(
            SparkError::NullMapKey.exception_class(),
            "org/apache/spark/SparkRuntimeException"
        );
    }
}
