// 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::array::{Array, Decimal128Array, Int32Array, Time64NanosecondArray};
use arrow::compute::cast;
use arrow::datatypes::{DataType, TimeUnit};
use datafusion::common::{utils::take_function_args, DataFusionError, Result};
use datafusion::logical_expr::{
    ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, Volatility,
};
use std::any::Any;
use std::sync::Arc;

const MICROS_PER_SECOND: i128 = 1_000_000;
const NANOS_PER_MICRO: i64 = 1_000;
const NANOS_PER_SECOND: i64 = 1_000_000_000;

#[derive(Debug, PartialEq, Eq, Hash)]
pub struct SparkMakeTime {
    signature: Signature,
}

impl SparkMakeTime {
    pub fn new() -> Self {
        Self {
            signature: Signature::any(3, Volatility::Immutable),
        }
    }
}

impl Default for SparkMakeTime {
    fn default() -> Self {
        Self::new()
    }
}

/// Converts hours, minutes, and fractional seconds (Decimal(16,6)) to nanoseconds from midnight.
/// Returns an error for invalid inputs (matching Spark's always-throw behavior).
fn make_time(hours: i32, minutes: i32, secs_and_micros_unscaled: i128) -> Result<i64> {
    let full_secs = secs_and_micros_unscaled.div_euclid(MICROS_PER_SECOND);
    let frac_micros = secs_and_micros_unscaled.rem_euclid(MICROS_PER_SECOND);

    if full_secs > i32::MAX as i128 || full_secs < 0 {
        return Err(DataFusionError::Execution(format!(
            "Invalid value for SecondOfMinute (valid values 0 - 59): {}",
            secs_and_micros_unscaled / MICROS_PER_SECOND
        )));
    }

    let secs = full_secs as i32;
    let nanos = (frac_micros as i64) * NANOS_PER_MICRO;

    if !(0..=23).contains(&hours) {
        return Err(DataFusionError::Execution(format!(
            "Invalid value for HourOfDay (valid values 0 - 23): {hours}"
        )));
    }
    if !(0..=59).contains(&minutes) {
        return Err(DataFusionError::Execution(format!(
            "Invalid value for MinuteOfHour (valid values 0 - 59): {minutes}"
        )));
    }
    if !(0..=59).contains(&secs) {
        return Err(DataFusionError::Execution(format!(
            "Invalid value for SecondOfMinute (valid values 0 - 59): {secs}"
        )));
    }

    let total_nanos = hours as i64 * 3_600 * NANOS_PER_SECOND
        + minutes as i64 * 60 * NANOS_PER_SECOND
        + secs as i64 * NANOS_PER_SECOND
        + nanos;

    Ok(total_nanos)
}

impl ScalarUDFImpl for SparkMakeTime {
    fn as_any(&self) -> &dyn Any {
        self
    }

    fn name(&self) -> &str {
        "make_time"
    }

    fn signature(&self) -> &Signature {
        &self.signature
    }

    fn return_type(&self, _: &[DataType]) -> Result<DataType> {
        Ok(DataType::Time64(TimeUnit::Nanosecond))
    }

    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
        let [hours, minutes, secs_and_micros] = take_function_args(self.name(), args.args)?;

        let num_rows = [&hours, &minutes, &secs_and_micros]
            .iter()
            .find_map(|arg| match arg {
                ColumnarValue::Array(array) => Some(array.len()),
                ColumnarValue::Scalar(_) => None,
            })
            .unwrap_or(1);

        let hours_arr = hours.into_array(num_rows)?;
        let minutes_arr = minutes.into_array(num_rows)?;
        let secs_arr = secs_and_micros.into_array(num_rows)?;

        let hours_arr = cast_to_int32(&hours_arr)?;
        let minutes_arr = cast_to_int32(&minutes_arr)?;

        let hours_array = hours_arr
            .as_any()
            .downcast_ref::<Int32Array>()
            .ok_or_else(|| {
                DataFusionError::Execution("make_time: failed to cast hours to Int32".to_string())
            })?;

        let minutes_array = minutes_arr
            .as_any()
            .downcast_ref::<Int32Array>()
            .ok_or_else(|| {
                DataFusionError::Execution("make_time: failed to cast minutes to Int32".to_string())
            })?;

        let secs_array = secs_arr
            .as_any()
            .downcast_ref::<Decimal128Array>()
            .ok_or_else(|| {
                DataFusionError::Execution(
                    "make_time: expected Decimal128 for seconds argument".to_string(),
                )
            })?;

        let len = hours_array.len();
        let mut builder = Time64NanosecondArray::builder(len);

        for i in 0..len {
            if hours_array.is_null(i) || minutes_array.is_null(i) || secs_array.is_null(i) {
                builder.append_null();
            } else {
                let h = hours_array.value(i);
                let m = minutes_array.value(i);
                let s = secs_array.value(i);

                let nanos = make_time(h, m, s)?;
                builder.append_value(nanos);
            }
        }

        Ok(ColumnarValue::Array(Arc::new(builder.finish())))
    }
}

fn cast_to_int32(arr: &Arc<dyn Array>) -> Result<Arc<dyn Array>> {
    if arr.data_type() == &DataType::Int32 {
        Ok(Arc::clone(arr))
    } else {
        cast(arr.as_ref(), &DataType::Int32)
            .map_err(|e| DataFusionError::Execution(format!("Failed to cast to Int32: {e}")))
    }
}

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

    #[test]
    fn test_make_time_valid() {
        // Midnight
        assert_eq!(make_time(0, 0, 0).unwrap(), 0);
        // 1 hour
        assert_eq!(make_time(1, 0, 0).unwrap(), 3_600_000_000_000);
        // 1 minute
        assert_eq!(make_time(0, 1, 0).unwrap(), 60_000_000_000);
        // 1 second (unscaled: 1_000_000)
        assert_eq!(make_time(0, 0, 1_000_000).unwrap(), 1_000_000_000);
        // 1.5 seconds (unscaled: 1_500_000)
        assert_eq!(make_time(0, 0, 1_500_000).unwrap(), 1_500_000_000);
        // 23:59:59.999999 (unscaled: 59_999_999)
        assert_eq!(make_time(23, 59, 59_999_999).unwrap(), 86_399_999_999_000);
        // 12:30:45.123456 (unscaled: 45_123_456)
        assert_eq!(
            make_time(12, 30, 45_123_456).unwrap(),
            12 * 3_600_000_000_000 + 30 * 60_000_000_000 + 45_123_456_000
        );
    }

    #[test]
    fn test_make_time_invalid_hours() {
        assert!(make_time(24, 0, 0).is_err());
        assert!(make_time(25, 0, 0).is_err());
        assert!(make_time(-1, 0, 0).is_err());
    }

    #[test]
    fn test_make_time_invalid_minutes() {
        assert!(make_time(0, 60, 0).is_err());
        assert!(make_time(0, -1, 0).is_err());
    }

    #[test]
    fn test_make_time_invalid_seconds() {
        // 60 seconds (unscaled: 60_000_000)
        assert!(make_time(0, 0, 60_000_000).is_err());
        // 100.5 seconds (unscaled: 100_500_000)
        assert!(make_time(0, 0, 100_500_000).is_err());
        // negative seconds (unscaled: -1_000_000)
        assert!(make_time(0, 0, -1_000_000).is_err());
    }

    #[test]
    fn test_make_time_overflow_seconds() {
        // Very large value that overflows i32
        let large = (i32::MAX as i128 + 1) * MICROS_PER_SECOND;
        assert!(make_time(0, 0, large).is_err());
    }
}
