//! Translation implementations for datetime functions

use crate::utils::{direct_function_translation, wrap_timestamp};
use crate::TranslationRegistry;
use hamelin_lib::func::defs::{
    AtTimezone, Day, DayOfWeek, FromMillis, FromNanos, FromUnixtimeMicros, FromUnixtimeMillis,
    FromUnixtimeNanos, FromUnixtimeSeconds, Hour, Minute, Month, Now, Second, ToMillis, ToNanos,
    ToUnixtime, Today, Tomorrow, Ts, Year, Yesterday,
};
use hamelin_lib::sql::expression::apply::FunctionCallApply;
use hamelin_lib::sql::expression::extract::{ExtractField, ExtractFunction};
use hamelin_lib::sql::expression::literal::{
    IntegerLiteral, IntervalLiteral, StringLiteral, TimestampLiteral, Unit,
};
use hamelin_lib::sql::expression::operator::Operator;
use hamelin_lib::sql::expression::{apply::BinaryOperatorApply, Cast, Leaf, SQLExpression};
use hamelin_lib::sql::types::{SQLBaseType, SQLTimestampTzType};

/// Register all datetime function translations.
pub fn register(registry: &mut TranslationRegistry) {
    // now() - pass through
    registry.register::<Now>(direct_function_translation);

    // today() - date_trunc('day', now())
    registry.register::<Today>(|_, _| {
        Ok(FunctionCallApply::with_two(
            "date_trunc",
            StringLiteral::new("day").into(),
            FunctionCallApply::with_no_arguments("now").into(),
        )
        .into())
    });

    // yesterday() - date_trunc('day', now() - interval '1' day)
    registry.register::<Yesterday>(|_, _| {
        Ok(FunctionCallApply::with_two(
            "date_trunc",
            StringLiteral::new("day").into(),
            BinaryOperatorApply::new(
                Operator::Minus,
                FunctionCallApply::with_no_arguments("now").into(),
                IntervalLiteral::new("1", Unit::Day).into(),
            )
            .into(),
        )
        .into())
    });

    // tomorrow() - date_trunc('day', now() + interval '1' day)
    registry.register::<Tomorrow>(|_, _| {
        Ok(FunctionCallApply::with_two(
            "date_trunc",
            StringLiteral::new("day").into(),
            BinaryOperatorApply::new(
                Operator::Plus,
                FunctionCallApply::with_no_arguments("now").into(),
                IntervalLiteral::new("1", Unit::Day).into(),
            )
            .into(),
        )
        .into())
    });

    // ts(string) - parse timestamp, cast to TIMESTAMP(6) WITH TIME ZONE for microsecond precision
    registry.register::<Ts>(|_, mut bindings| {
        let timestamp = bindings.take()?;
        let parsed = match timestamp.sql {
            SQLExpression::Leaf(Leaf::StringLiteral(l)) => {
                TimestampLiteral::from_string_literal(&l)?.into()
            }
            other => wrap_timestamp(other),
        };
        // Cast to microsecond precision to standardize timestamp precision
        Ok(Cast::new(parsed, SQLTimestampTzType::new(6).into()).into())
    });

    // year(timestamp) - extract year
    registry.register::<Year>(|_, mut bindings| {
        let timestamp = bindings.take()?;
        Ok(ExtractFunction::new(ExtractField::Year, timestamp.sql).into())
    });

    // month(timestamp) - extract month
    registry.register::<Month>(|_, mut bindings| {
        let timestamp = bindings.take()?;
        Ok(ExtractFunction::new(ExtractField::Month, timestamp.sql).into())
    });

    // day(timestamp) - extract day
    registry.register::<Day>(|_, mut bindings| {
        let timestamp = bindings.take()?;
        Ok(ExtractFunction::new(ExtractField::Day, timestamp.sql).into())
    });

    // day_of_week(timestamp) - extract day of week
    registry.register::<DayOfWeek>(|_, mut bindings| {
        let timestamp = bindings.take()?;
        Ok(ExtractFunction::new(ExtractField::DayOfWeek, timestamp.sql).into())
    });

    // hour(timestamp) - extract hour
    registry.register::<Hour>(|_, mut bindings| {
        let timestamp = bindings.take()?;
        Ok(ExtractFunction::new(ExtractField::Hour, timestamp.sql).into())
    });

    // minute(timestamp) - extract minute
    registry.register::<Minute>(|_, mut bindings| {
        let timestamp = bindings.take()?;
        Ok(ExtractFunction::new(ExtractField::Minute, timestamp.sql).into())
    });

    // second(timestamp) - extract second
    registry.register::<Second>(|_, mut bindings| {
        let timestamp = bindings.take()?;
        Ok(ExtractFunction::new(ExtractField::Second, timestamp.sql).into())
    });

    // at_timezone(timestamp, timezone) - convert timezone
    registry.register::<AtTimezone>(|_, mut bindings| {
        let timestamp = bindings.take()?;
        let timezone = bindings.take()?;
        Ok(FunctionCallApply::with_two("at_timezone", timestamp.sql, timezone.sql).into())
    });

    // to_millis(interval) - to_milliseconds()
    registry.register::<ToMillis>(|_, mut bindings| {
        let interval = bindings.take()?;
        Ok(FunctionCallApply::with_one("to_milliseconds", interval.sql).into())
    });

    // to_nanos(interval) - to_milliseconds() * 1000000
    registry.register::<ToNanos>(|_, mut bindings| {
        let interval = bindings.take()?;
        Ok(BinaryOperatorApply::new(
            Operator::Asterisk,
            FunctionCallApply::with_one("to_milliseconds", interval.sql).into(),
            IntegerLiteral::from_int(1000000).into(),
        )
        .into())
    });

    // from_millis(millis) - (millis / 1000.0) * interval '1' second
    registry.register::<FromMillis>(|_, mut bindings| {
        let millis = bindings.take()?;
        Ok(BinaryOperatorApply::new(
            Operator::Asterisk,
            BinaryOperatorApply::new(
                Operator::Slash,
                Cast::new(millis.sql, SQLBaseType::Double.into()).into(),
                IntegerLiteral::from_int(1000).into(),
            )
            .into(),
            IntervalLiteral::new("1", Unit::Second).into(),
        )
        .into())
    });

    // from_nanos(nanos) - (nanos / 1000000000.0) * interval '1' second
    registry.register::<FromNanos>(|_, mut bindings| {
        let nanos = bindings.take()?;
        Ok(BinaryOperatorApply::new(
            Operator::Asterisk,
            BinaryOperatorApply::new(
                Operator::Slash,
                Cast::new(nanos.sql, SQLBaseType::Double.into()).into(),
                IntegerLiteral::from_int(1000000000).into(),
            )
            .into(),
            IntervalLiteral::new("1", Unit::Second).into(),
        )
        .into())
    });

    // from_unixtime_seconds(seconds) - from_unixtime()
    registry.register::<FromUnixtimeSeconds>(|_, mut bindings| {
        let seconds = bindings.take()?;
        Ok(FunctionCallApply::with_one("from_unixtime", seconds.sql).into())
    });

    // from_unixtime_millis(millis) - from_unixtime_nanos(millis * 1000000)
    registry.register::<FromUnixtimeMillis>(|_, mut bindings| {
        let millis = bindings.take()?;
        Ok(FunctionCallApply::with_one(
            "from_unixtime_nanos",
            BinaryOperatorApply::new(
                Operator::Asterisk,
                millis.sql,
                IntegerLiteral::from_int(1000000).into(),
            )
            .into(),
        )
        .into())
    });

    // from_unixtime_micros(micros) - from_unixtime_nanos(micros * 1000)
    registry.register::<FromUnixtimeMicros>(|_, mut bindings| {
        let micros = bindings.take()?;
        Ok(FunctionCallApply::with_one(
            "from_unixtime_nanos",
            BinaryOperatorApply::new(
                Operator::Asterisk,
                micros.sql,
                IntegerLiteral::from_int(1000).into(),
            )
            .into(),
        )
        .into())
    });

    // from_unixtime_nanos(nanos) - wrap with cast to TIMESTAMP(6) WITH TIME ZONE
    // Trino's from_unixtime_nanos returns nanosecond precision, but Hamelin standardizes on microseconds
    registry.register::<FromUnixtimeNanos>(|name, bindings| {
        let inner = direct_function_translation(name, bindings)?;
        Ok(Cast::new(inner, SQLTimestampTzType::new(6).into()).into())
    });

    // to_unixtime(timestamp) - pass through
    registry.register::<ToUnixtime>(direct_function_translation);
}
