"""
Date and time manipulation functions.

This module provides comprehensive date and datetime operations including:
- Date construction, parsing, and formatting
- DateTime construction, parsing, and formatting
- Date and time component extraction (year, month, day, hour, minute, second)
- Date and time arithmetic (add, subtract)
- Date and time ranges
- Time period constructors (days, hours, minutes, seconds, etc.)
- ISO format constants for standardized formatting
"""
from __future__ import annotations


from . import StringValue, IntegerValue, DateValue, DateTimeValue, math, common
from ..frontend.base import Library, MetaRef, Expression, Field, Variable, Relationship
from ..frontend.core import Float, Number, String, Integer, Date, DateTime
from ..frontend import core
from .. import select
from relationalai.util.docutils import include_in_docs

from typing import Union, Literal as LiteralType
import datetime as dt

__include_in_docs__ = True

# the front-end library object
library = Library("datetime")


#--------------------------------------------------
# Format String Constants
#--------------------------------------------------

@include_in_docs
class ISO:
    """
    ISO format string constants for date and datetime formatting.

    Attributes
    ----------
    DATE : str
        Date format: "yyyy-mm-dd"
    HOURS : str
        Datetime with hours: "yyyy-mm-ddTHH"
    HOURS_TZ : str
        Datetime with hours and timezone: "yyyy-mm-ddTHHz"
    MINUTES : str
        Datetime with minutes: "yyyy-mm-ddTHH:MM"
    MINUTES_TZ : str
        Datetime with minutes and timezone: "yyyy-mm-ddTHH:MMz"
    SECONDS : str
        Datetime with seconds: "yyyy-mm-ddTHH:MM:SS"
    SECONDS_TZ : str
        Datetime with seconds and timezone: "yyyy-mm-ddTHH:MM:SSz"
    MILLIS : str
        Datetime with milliseconds: "yyyy-mm-ddTHH:MM:SS.s"
    MILLIS_TZ : str
        Datetime with milliseconds and timezone: "yyyy-mm-ddTHH:MM:SS.sz"
    """
    DATE = "yyyy-mm-dd"
    HOURS = "yyyy-mm-ddTHH"
    HOURS_TZ = "yyyy-mm-ddTHHz"
    MINUTES = "yyyy-mm-ddTHH:MM"
    MINUTES_TZ = "yyyy-mm-ddTHH:MMz"
    SECONDS = "yyyy-mm-ddTHH:MM:SS"
    SECONDS_TZ = "yyyy-mm-ddTHH:MM:SSz"
    MILLIS = "yyyy-mm-ddTHH:MM:SS.s"
    MILLIS_TZ = "yyyy-mm-ddTHH:MM:SS.sz"



#--------------------------------------------------
# Date
#--------------------------------------------------

# Constructors
_construct_date = library.Relation("construct_date", [Field.input("year", Integer), Field.input("month", Integer), Field.input("day", Integer), Field("date", Date)])
_construct_date_from_datetime = library.Relation("construct_date_from_datetime", [Field.input("datetime", DateTime), Field.input("timezone", String), Field("date", Date)])
_construct_datetime_ms_tz = library.Relation("construct_datetime_ms_tz", [
    Field.input("year", Integer), Field.input("month", Integer), Field.input("day", Integer),
    Field.input("hour", Integer), Field.input("minute", Integer), Field.input("second", Integer), Field.input("milliseconds", Integer),
    Field.input("timezone", String),
    Field("datetime", DateTime)]
)
_parse_date = library.Relation("parse_date", [Field.input("date_string", String), Field.input("format", String), Field("date", Date)])

# Formatting
_date_format = library.Relation("date_format", [Field.input("date", Date), Field.input("format", String), Field("result", String)])

# Extractors
_date_year = library.Relation("date_year", [Field.input("date", Date), Field("year", Number.size(19, 0))])
_date_quarter = library.Relation("date_quarter", [Field.input("date", Date), Field("quarter", Integer)])
_date_month = library.Relation("date_month", [Field.input("date", Date), Field("month", Integer)])
_date_week = library.Relation("date_week", [Field.input("date", Date), Field("week", Integer)])
_date_day = library.Relation("date_day", [Field.input("date", Date), Field("day", Integer)])
_date_dayofyear = library.Relation("date_dayofyear", [Field.input("date", Date), Field("dayofyear", Integer)])
_date_weekday = library.Relation("date_weekday", [Field.input("date", Date), Field("weekday", Integer)])

# Date Operations
_date_add = library.Relation("date_add", [Field.input("date", Date), Field.input("period", Integer), Field("result", Date)])
_date_subtract = library.Relation("date_subtract", [Field.input("date", Date), Field.input("period", Integer), Field("result", Date)])
_date_diff = library.Relation("date_diff", [Field.input("part", String), Field.input("start", Date), Field.input("end", Date), Field("result", Integer)])

# Date Ranges
#   Still unimplemented because we need to add support for emitters to transform lookups using DSL itself. Also,
#   the API is not clear, it could be better to have date_range_from_date where periods can be negative to go backwards.
# _date_range = library.Relation("date_range", [Field.input("start", Date), Field.input("end", Date), Field.input("frequency", String), Field("date", Date)])
# _date_range_from_start = library.Relation("date_range_from_start", [Field.input("start", Date), Field.input("periods", Integer), Field.input("frequency", String), Field("date", Date)])
# _date_range_from_end = library.Relation("date_range_from_end", [Field.input("end", Date), Field.input("periods", Integer), Field.input("frequency", String), Field("date", Date)])

@include_in_docs
class date:
    """
    Construct a date from year, month, and day.

    When you construct a date using this class, it returns an `Expression` and not a `date`
    object. All of the methods on this class are class methods that operate on date
    expressions.

    Parameters
    ----------
    year: IntegerValue
        The year.
    month: IntegerValue
        The month (1-12).
    day: IntegerValue
        The day of the month.

    Examples
    --------
    Construct dates:

    >>> datetime.date(2024, 1, 15)
    >>> datetime.date(Employee.hire_year, Employee.hire_month, Employee.hire_day)
    """

    def __new__(cls, year: IntegerValue, month: IntegerValue, day: IntegerValue) -> Expression:
        return _construct_date(year, month, day)

    def __init__(self, year: IntegerValue, month: IntegerValue, day: IntegerValue):
        pass

    @classmethod
    @include_in_docs
    def year(cls, date: DateValue) -> Expression:
        """
        Extract the year from a date.

        Parameters
        ----------
        date: DateValue
            The date to extract from.

        Returns
        -------
        Expression
            An `Expression` computing the year. Returns `Integer`.

        Examples
        --------
        Extract year from dates:

        >>> datetime.date.year(Event.start_date)
        >>> select(Person.name).where(datetime.date.year(Person.birth_date) == 2000)
        """
        return _date_year(date)

    @classmethod
    @include_in_docs
    def quarter(cls, date: DateValue) -> Expression:
        """
        Extract the quarter (1-4) from a date.

        Parameters
        ----------
        date: DateValue
            The date to extract from.

        Returns
        -------
        Expression
            An `Expression` computing the quarter. Returns `Integer`.
        """
        return _date_quarter(date)

    @classmethod
    @include_in_docs
    def month(cls, date: DateValue) -> Expression:
        """
        Extract the month (1-12) from a date.

        Parameters
        ----------
        date: DateValue
            The date to extract from.

        Returns
        -------
        Expression
            An `Expression` computing the month. Returns `Integer`.
        """
        return _date_month(date)

    @classmethod
    @include_in_docs
    def week(cls, date: DateValue) -> Expression:
        """
        Extract the ISO week number from a date.

        Parameters
        ----------
        date: DateValue
            The date to extract from.

        Returns
        -------
        Expression
            An `Expression` computing the week number. Returns `Integer`.
        """
        return _date_week(date)

    @classmethod
    @include_in_docs
    def day(cls, date: DateValue) -> Expression:
        """
        Extract the day of the month from a date.

        Parameters
        ----------
        date: DateValue
            The date to extract from.

        Returns
        -------
        Expression
            An `Expression` computing the day. Returns `Integer`.
        """
        return _date_day(date)

    @classmethod
    @include_in_docs
    def dayofyear(cls, date: DateValue) -> Expression:
        """
        Extract the day of the year (1-366) from a date.

        Parameters
        ----------
        date: DateValue
            The date to extract from.

        Returns
        -------
        Expression
            An `Expression` computing the day of year. Returns `Integer`.
        """
        return _date_dayofyear(date)

    @classmethod
    @include_in_docs
    def isoweekday(cls, date: DateValue) -> Expression:
        """
        Return the ISO weekday as an integer, where Monday is 1, and Sunday is 7.

        Parameters
        ----------
        date: DateValue
            The date to extract from.

        Returns
        -------
        Expression
            An `Expression` computing the ISO weekday (1=Monday...7=Sunday). Returns `Integer`.
        """
        return _date_weekday(date)

    @classmethod
    @include_in_docs
    def weekday(cls, date: DateValue) -> Expression:
        """
        Return the weekday as an integer, where Monday is 0, and Sunday is 6.

        Parameters
        ----------
        date: DateValue
            The date to extract from.

        Returns
        -------
        Expression
            An `Expression` computing the weekday (0=Monday...6=Sunday). Returns `Integer`.
        """
        return cls.isoweekday(date) - 1 # Convert ISO weekday (1=Mon..7=Sun) to weekday (0=Mon..6=Sun)

    @classmethod
    @include_in_docs
    def fromordinal(cls, ordinal: IntegerValue) -> Expression:
        """
        Construct a date from a proleptic Gregorian ordinal.

        Parameters
        ----------
        ordinal: IntegerValue
            The ordinal (day 1 is 0001-01-01).

        Returns
        -------
        Expression
            An `Expression` computing the date. Returns `Date`.
        """
        # ordinal 1 = '0001-01-01'. Minus 1 day since we can't declare date 0000-00-00
        return cls.add(Date(dt.date(1, 1, 1)), days(ordinal - 1))

    @classmethod
    @include_in_docs
    def to_datetime(cls, date: DateValue, hour: int = 0, minute: int = 0, second: int = 0, millisecond: int = 0, tz: str = "UTC") -> Expression:
        """
        Convert a date to a datetime with specified time components.

        Parameters
        ----------
        date: DateValue
            The date to convert.
        hour: int
            The hour (0-23). Default: 0.
        minute: int
            The minute (0-59). Default: 0.
        second: int
            The second (0-59). Default: 0.
        millisecond: int
            The millisecond (0-999). Default: 0.
        tz: str
            The timezone string. Default: "UTC".

        Returns
        -------
        Expression
            An `Expression` computing the datetime. Returns `DateTime`.
        """
        _year = cls.year(date)
        _month = cls.month(date)
        _day = cls.day(date)
        return _construct_datetime_ms_tz(_year, _month, _day, hour, minute, second, millisecond, tz)

    @classmethod
    @include_in_docs
    def format(cls, date: DateValue, format: StringValue) -> Expression:
        """
        Format a date as a string.

        Parameters
        ----------
        date: DateValue
            The date to format.
        format: StringValue
            The format string.

        Returns
        -------
        Expression
            An `Expression` computing the formatted date string. Returns `String`.
        """
        return _date_format(date, format)

    @classmethod
    @include_in_docs
    def add(cls, date: DateValue, period: Variable) -> Expression:
        """
        Add a period to a date.

        Parameters
        ----------
        date: DateValue
            The date to add to.
        period: Variable
            The period to add (use `days()`, `weeks()`, `months()`, `years()`).

        Returns
        -------
        Expression
            An `Expression` computing the resulting date. Returns `Date`. Returns `Date`.

        Examples
        --------
        Add periods to dates:

        >>> datetime.date.add(Order.order_date, datetime.days(7))
        >>> datetime.date.add(Project.start_date, datetime.weeks(Contract.duration_weeks))
        >>> datetime.date.add(Subscription.start_date, datetime.months(12))
        """
        return _date_add(date, period)

    @classmethod
    @include_in_docs
    def subtract(cls, date: DateValue, period: Variable) -> Expression:
        """
        Subtract a period from a date.

        Parameters
        ----------
        date: DateValue
            The date to subtract from.
        period: Variable
            The period to subtract (use `days()`, `weeks()`, `months()`, `years()`).

        Returns
        -------
        Expression
            An `Expression` computing the resulting date. Returns `Date`.
        """
        return _date_subtract(date, period)

    @classmethod
    @include_in_docs
    def range(cls, start: DateValue | None = None, end: DateValue | None = None, periods: IntegerValue = 1, freq: Frequency = "D") -> Variable:
        """
        Generate a range of dates.

        Parameters
        ----------
        start: DateValue | None
            The start date. If None, computes from end.
        end: DateValue | None
            The end date (inclusive). If None, uses periods.
        periods: IntegerValue
            Number of periods to generate. Default: 1.
        freq: Frequency
            Frequency: "D" (day), "W" (week), "M" (month), "Y" (year). Default: "D".

        Returns
        -------
        Variable
            A `Variable` representing the date range. Returns `Date`.

        Raises
        ------
        ValueError
            If neither start nor end is provided, or if freq is invalid.
        """
        if start is None and end is None:
            raise ValueError("Invalid start/end date for date.range. Must provide at least start date or end date")
        if freq not in _days.keys():
            raise ValueError(f"Frequency '{freq}' is not allowed for date_range. List of allowed frequencies: {list(_days.keys())}")

        # Note on date_ranges and datetime_range: The way the computation works is that it first overapproximates the
        # number of periods.

        # For example, date_range(2025-02-01, 2025-03-01, freq='M') and date_range(2025-02-01, 2025-03-31, freq='M') will
        # compute range_end to be ceil(28*1/(365/12))=1 and ceil(58*1/(365/12))=2.

        # Then, the computation fetches range_end+1 items into _date, which is the right number in the first case but
        # one too many in the second case. That's why a filter end >= _date (or variant of) is applied, to remove any
        # extra item. The result is two items in both cases.

        # TODO - this transformation is currently LQP-focused. Eventually we will want to
        # move it into the LQP stack and have something general here.
        date_func = cls.add
        if start is None:
            # compute end - periods*freq
            start = end
            end = None
            date_func = cls.subtract
        assert start is not None
        if end is not None:
            num_days = cls.period_days(start, end)
            if freq in ["W", "M", "Y"]:
                range_end = math.ceil(num_days * _days[freq])
                range_end = core.cast(MetaRef(Integer), range_end)
            else:
                range_end = num_days
            # date_range is inclusive. add 1 since std.range is exclusive
            ix = common.range(0, range_end + 1, 1)
        else:
            ix = common.range(0, periods, 1)
        _date = date_func(start, _periods[freq](ix))
        if isinstance(end, dt.date) :
            return select(_date).where(Date(end) >= _date)
        elif end is not None:
            return select(_date).where(end >= _date)
        return _date


    @classmethod
    @include_in_docs
    def period_days(cls, start: DateValue, end: DateValue) -> Expression:
        """
        Compute the number of days between two dates.

        Parameters
        ----------
        start: DateValue
            The start date.
        end: DateValue
            The end date.

        Returns
        -------
        Expression
            An `Expression` computing the number of days. Returns `Integer`.
        """
        return _dates_period_days(start, end)

    @classmethod
    @include_in_docs
    def diff(cls, part: DatePart, start: DateValue, end: DateValue) -> Expression:
        """
        Calculate the difference between two dates in the specified date part units.

        Parameters
        ----------
        part: DatePart
            The unit to measure the difference in: "year", "quarter", "month", "week", or "day".
        start: DateValue
            The start date.
        end: DateValue
            The end date.

        Returns
        -------
        Expression
            An `Expression` computing the difference. Returns `Integer`.

        Examples
        --------
        Calculate date differences:

        >>> datetime.date.diff("day", Order.start_date, Order.end_date)
        >>> datetime.date.diff("month", Employee.hire_date, Employee.termination_date)
        >>> datetime.date.diff("week", Project.start, Project.finish)
        """
        return _date_diff(part, start, end)

    @classmethod
    @include_in_docs
    def fromisoformat(cls, date_string: StringValue) -> Expression:
        """
        Parse a date from ISO format string (yyyy-mm-dd).

        Parameters
        ----------
        date_string: StringValue
            The ISO format date string.

        Returns
        -------
        Expression
            An `Expression` computing the parsed date. Returns `Date`.
        """
        return _parse_date(date_string, ISO.DATE)

#--------------------------------------------------
# DateTime
#--------------------------------------------------

# Constructors
_construct_datetime_ms_tz = library.Relation("construct_datetime_ms_tz", [
    Field.input("year", Integer), Field.input("month", Integer), Field.input("day", Integer),
    Field.input("hour", Integer), Field.input("minute", Integer), Field.input("second", Integer), Field.input("milliseconds", Integer),
    Field.input("timezone", String),
    Field("datetime", DateTime)]
)
_datetime_now = library.Relation("datetime_now", [Field("datetime", DateTime)])
_parse_datetime = library.Relation("parse_datetime", [Field.input("datetime_string", String), Field.input("format", String), Field("datetime", DateTime)])

# Formatting
_datetime_format = library.Relation("datetime_format", [Field.input("datetime", DateTime), Field.input("format", String), Field.input("timezone", String), Field("result", String)])

# Extractors
_datetime_year = library.Relation("datetime_year", [Field.input("datetime", DateTime), Field.input("timezone", String), Field("year", Integer)])
_datetime_quarter = library.Relation("datetime_quarter", [Field.input("datetime", DateTime), Field.input("timezone", String), Field("quarter", Integer)])
_datetime_month = library.Relation("datetime_month", [Field.input("datetime", DateTime), Field.input("timezone", String), Field("month", Integer)])
_datetime_week = library.Relation("datetime_week", [Field.input("datetime", DateTime), Field.input("timezone", String), Field("week", Integer)])
_datetime_day = library.Relation("datetime_day", [Field.input("datetime", DateTime), Field.input("timezone", String), Field("day", Integer)])
_datetime_dayofyear = library.Relation("datetime_dayofyear", [Field.input("datetime", DateTime), Field.input("timezone", String), Field("dayofyear", Integer)])
_datetime_hour = library.Relation("datetime_hour", [Field.input("datetime", DateTime), Field.input("timezone", String), Field("hour", Integer)])
_datetime_minute = library.Relation("datetime_minute", [Field.input("datetime", DateTime), Field.input("timezone", String), Field("minute", Integer)])
# no timezone needed for second extraction
_datetime_second = library.Relation("datetime_second", [Field.input("datetime", DateTime), Field("second", Integer)])
_datetime_weekday = library.Relation("datetime_weekday", [Field.input("datetime", DateTime), Field.input("timezone", String), Field("weekday", Integer)])

# DateTime Operations
_datetime_add = library.Relation("datetime_add", [Field.input("datetime", DateTime), Field.input("period", Integer), Field("result", DateTime)])
_datetime_subtract = library.Relation("datetime_subtract", [Field.input("datetime", DateTime), Field.input("period", Integer), Field("result", DateTime)])
_datetime_diff = library.Relation("datetime_diff", [Field.input("part", String), Field.input("start", DateTime), Field.input("end", DateTime), Field("result", Integer)])

# DateTime Ranges (see comment on Date Ranges)
# _datetime_range = library.Relation("datetime_range", [Field.input("start", DateTime), Field.input("end", DateTime), Field.input("frequency", String), Field("datetime", DateTime)])
# _datetime_range_from_start = library.Relation("datetime_range_from_start", [Field.input("start", DateTime), Field.input("periods", Integer), Field.input("frequency", String), Field("datetime", DateTime)])
# _datetime_range_from_end = library.Relation("datetime_range_from_end", [Field.input("end", DateTime), Field.input("periods", Integer), Field.input("frequency", String), Field("datetime", DateTime)])

@include_in_docs
class datetime:
    """
    Construct a datetime from components.

    When you construct a datetime using this class, it returns an `Expression` and not a
    `datetime` object. All of the methods on this class are class methods that operate on
    datetime expressions.

    Parameters
    ----------
    year: IntegerValue
        The year.
    month: IntegerValue
        The month (1-12).
    day: IntegerValue
        The day of the month.
    hour: IntegerValue
        The hour (0-23). Default: 0.
    minute: IntegerValue
        The minute (0-59). Default: 0.
    second: IntegerValue
        The second (0-59). Default: 0.
    millisecond: IntegerValue
        The millisecond (0-999). Default: 0.
    tz: dt.tzinfo | StringValue
        The timezone. Default: "UTC".

    Examples
    --------
    Construct datetimes:

    >>> datetime.datetime(2024, 1, 15, 14, 30, 0)
    >>> datetime.datetime(2024, 1, 1, 6, 30, 45, 0, 'America/New_York')
    >>> datetime.datetime(Event.year, Event.month, Event.day, Event.hour, Event.minute)
    """

    def __new__(cls, year: IntegerValue, month: IntegerValue, day: IntegerValue, hour: IntegerValue = 0, minute: IntegerValue = 0,
             second: IntegerValue = 0, millisecond: IntegerValue = 0, tz: dt.tzinfo|StringValue = "UTC") -> Expression:
        if isinstance(tz, dt.tzinfo):
            tz = str(tz)
        return _construct_datetime_ms_tz(year, month, day, hour, minute, second, millisecond, tz)

    def __init__(self, year: IntegerValue, month: IntegerValue, day: IntegerValue, hour: IntegerValue = 0, minute: IntegerValue = 0,
             second: IntegerValue = 0, millisecond: IntegerValue = 0, tz: dt.tzinfo|StringValue = "UTC"):
        pass

    @classmethod
    @include_in_docs
    def now(cls) -> Relationship:
        """
        Get the current datetime.

        Returns
        -------
        Expression
            An `Expression` computing the current datetime. Returns `DateTime`.
        """
        return _datetime_now

    @classmethod
    @include_in_docs
    def year(cls, datetime: DateTimeValue, tz: dt.tzinfo|StringValue|None = None) -> Expression:
        """
        Extract the year from a datetime.

        Parameters
        ----------
        datetime: DateTimeValue
            The datetime to extract from.
        tz: dt.tzinfo | StringValue | None
            The timezone for extraction. Default: None (uses datetime's timezone or UTC).

        Returns
        -------
        Expression
            An Expression computing the year.
        """
        tz = _extract_tz(datetime, tz)
        return _datetime_year(datetime, tz)

    @classmethod
    @include_in_docs
    def quarter(cls, datetime: DateTimeValue,  tz: dt.tzinfo|StringValue|None = None) -> Expression:
        """
        Extract the quarter (1-4) from a datetime.

        Parameters
        ----------
        datetime: DateTimeValue
            The datetime to extract from.
        tz: dt.tzinfo | StringValue | None
            The timezone for extraction. Default: None.

        Returns
        -------
        Expression
            An `Expression` computing the quarter. Returns `Integer`.
        """
        tz = _extract_tz(datetime, tz)
        return _datetime_quarter(datetime, tz)

    @classmethod
    @include_in_docs
    def month(cls, datetime: DateTimeValue, tz: dt.tzinfo|StringValue|None = None) -> Expression:
        """
        Extract the month (1-12) from a datetime.

        Parameters
        ----------
        datetime: DateTimeValue
            The datetime to extract from.
        tz: dt.tzinfo | StringValue | None
            The timezone for extraction. Default: None.

        Returns
        -------
        Expression
            An `Expression` computing the month. Returns `Integer`.
        """
        tz = _extract_tz(datetime, tz)
        return _datetime_month(datetime, tz)

    @classmethod
    @include_in_docs
    def week(cls, datetime: DateTimeValue, tz: dt.tzinfo|StringValue|None = None) -> Expression:
        """
        Extract the ISO week number from a datetime.

        Parameters
        ----------
        datetime: DateTimeValue
            The datetime to extract from.
        tz: dt.tzinfo | StringValue | None
            The timezone for extraction. Default: None.

        Returns
        -------
        Expression
            An `Expression` computing the week number. Returns `Integer`.
        """
        tz = _extract_tz(datetime, tz)
        return _datetime_week(datetime, tz)

    @classmethod
    @include_in_docs
    def day(cls, datetime: DateTimeValue, tz: dt.tzinfo|StringValue|None = None) -> Expression:
        """
        Extract the day of the month from a datetime.

        Parameters
        ----------
        datetime: DateTimeValue
            The datetime to extract from.
        tz: dt.tzinfo | StringValue | None
            The timezone for extraction. Default: None.

        Returns
        -------
        Expression
            An `Expression` computing the day. Returns `Integer`.
        """
        tz = _extract_tz(datetime, tz)
        return _datetime_day(datetime, tz)

    @classmethod
    @include_in_docs
    def dayofyear(cls, datetime: DateTimeValue, tz: dt.tzinfo|StringValue|None = None) -> Expression:
        """
        Extract the day of the year (1-366) from a datetime.

        Parameters
        ----------
        datetime: DateTimeValue
            The datetime to extract from.
        tz: dt.tzinfo | StringValue | None
            The timezone for extraction. Default: None.

        Returns
        -------
        Expression
            An `Expression` computing the day of year. Returns `Integer`.
        """
        tz = _extract_tz(datetime, tz)
        return _datetime_dayofyear(datetime, tz)

    @classmethod
    @include_in_docs
    def hour(cls, datetime: DateTimeValue, tz: dt.tzinfo|StringValue|None = None) -> Expression:
        """
        Extract the hour (0-23) from a datetime.

        Parameters
        ----------
        datetime: DateTimeValue
            The datetime to extract from.
        tz: dt.tzinfo | StringValue | None
            The timezone for extraction. Default: None.

        Returns
        -------
        Expression
            An `Expression` computing the hour. Returns `Integer`.
        """
        tz = _extract_tz(datetime, tz)
        return _datetime_hour(datetime, tz)

    @classmethod
    @include_in_docs
    def minute(cls, datetime: DateTimeValue, tz: dt.tzinfo|StringValue|None = None) -> Expression:
        """
        Extract the minute (0-59) from a datetime.

        Parameters
        ----------
        datetime: DateTimeValue
            The datetime to extract from.
        tz: dt.tzinfo | StringValue | None
            The timezone for extraction. Default: None.

        Returns
        -------
        Expression
            An `Expression` computing the minute. Returns `Integer`.
        """
        tz = _extract_tz(datetime, tz)
        return _datetime_minute(datetime, tz)

    @classmethod
    @include_in_docs
    def second(cls, datetime: DateTimeValue) -> Expression:
        """
        Extract the second (0-59) from a datetime.

        Parameters
        ----------
        datetime: DateTimeValue
            The datetime to extract from.

        Returns
        -------
        Expression
            An `Expression` computing the second. Returns `Integer`.
        """
        return _datetime_second(datetime)

    @classmethod
    @include_in_docs
    def isoweekday(cls, datetime: DateTimeValue, tz: dt.tzinfo|StringValue|None = None) -> Expression:
        """
        Return the ISO weekday as an integer, where Monday is 1, and Sunday is 7.

        Parameters
        ----------
        datetime: DateTimeValue
            The datetime to extract from.
        tz: dt.tzinfo | StringValue | None
            The timezone for extraction. Default: None.

        Returns
        -------
        Expression
            An `Expression` computing the ISO weekday (1=Monday...7=Sunday). Returns `Integer`.
        """
        tz = _extract_tz(datetime, tz)
        return _datetime_weekday(datetime, tz)

    @classmethod
    @include_in_docs
    def weekday(cls, datetime: DateTimeValue, tz: dt.tzinfo|StringValue|None = None) -> Expression:
        """
        Return the weekday as an integer, where Monday is 0, and Sunday is 6.

        Parameters
        ----------
        datetime: DateTimeValue
            The datetime to extract from.
        tz: dt.tzinfo | StringValue | None
            The timezone for extraction. Default: None.

        Returns
        -------
        Expression
            An `Expression` computing the weekday (0=Monday...6=Sunday). Returns `Integer`.
        """
        return cls.isoweekday(datetime, tz) - 1 # Convert ISO weekday (1=Mon..7=Sun) to weekday (0=Mon..6=Sun)

    @classmethod
    @include_in_docs
    def fromordinal(cls, ordinal: IntegerValue) -> Expression:
        """
        Construct a datetime from a proleptic Gregorian ordinal.

        Parameters
        ----------
        ordinal: IntegerValue
            The ordinal (day 1 is 0001-01-01).

        Returns
        -------
        Expression
            An `Expression` computing the datetime at midnight UTC. Returns `DateTime`.
        """
        # Convert ordinal to milliseconds, since ordinals in Python are days
        # Minus 1 day since we can't declare date 0000-00-00
        ordinal_milliseconds = (ordinal - 1) * 86400000 # 24 * 60 * 60 * 1000
        return cls.add(DateTime(dt.datetime(1, 1, 1, 0, 0, 0)), milliseconds(ordinal_milliseconds))

    @classmethod
    @include_in_docs
    def strptime(cls, date_str: StringValue, format: str) -> Expression:
        """
        Parse a datetime from a string using a format string.

        Parameters
        ----------
        date_str: StringValue
            The datetime string to parse.
        format: str
            The format string. Must be a literal Python string.

        Returns
        -------
        Expression
            An `Expression` computing the parsed datetime. Returns `DateTime`.
        """
        if not isinstance(format, str):
            raise TypeError("datetime.strptime requires a literal format string.")
        return _parse_datetime(date_str, format)

    @classmethod
    @include_in_docs
    def to_date(cls, datetime: DateTimeValue, tz: dt.tzinfo|StringValue|None = None) -> Expression:
        """
        Convert a datetime to a date.

        Parameters
        ----------
        datetime: DateTimeValue
            The datetime to convert.
        tz: dt.tzinfo | StringValue | None
            The timezone for conversion. Default: None.

        Returns
        -------
        Expression
            An `Expression` computing the date. Returns `Date`.
        """
        tz = _extract_tz(datetime, tz)
        return _construct_date_from_datetime(datetime, tz)

    @classmethod
    @include_in_docs
    def format(cls, date: DateTimeValue, format: StringValue, tz: dt.tzinfo|StringValue|None = None) -> Expression:
        """
        Format a datetime as a string.

        Parameters
        ----------
        date: DateTimeValue
            The datetime to format.
        format: StringValue
            The format string.
        tz: dt.tzinfo | StringValue | None
            The timezone for formatting. Default: None.

        Returns
        -------
        Expression
            An `Expression` computing the formatted datetime string. Returns `String`.
        """
        tz = _extract_tz(date, tz)
        return _datetime_format(date, format, tz)

    @classmethod
    @include_in_docs
    def add(cls, date: DateTimeValue, period: Variable) -> Expression:
        """
        Add a period to a datetime.

        Parameters
        ----------
        date: DateTimeValue
            The datetime to add to.
        period: Variable
            The period to add (use `milliseconds()`, `seconds()`, `minutes()`, `hours()`, `days()`, etc.).

        Returns
        -------
        Expression
            An `Expression` computing the resulting datetime. Returns `DateTime`. Returns `DateTime`.

        Examples
        --------
        Add periods to datetimes:

        >>> datetime.datetime.add(Event.start_time, datetime.hours(2))
        >>> datetime.datetime.add(Appointment.scheduled_at, datetime.minutes(30))
        >>> datetime.datetime.add(Task.created_at, datetime.days(7))
        """
        return _datetime_add(date, period)

    @classmethod
    @include_in_docs
    def subtract(cls, date: DateTimeValue, period: Variable) -> Expression:
        """
        Subtract a period from a datetime.

        Parameters
        ----------
        date: DateTimeValue
            The datetime to subtract from.
        period: Variable
            The period to subtract (use `milliseconds()`, `seconds()`, `minutes()`, `hours()`, `days()`, etc.).

        Returns
        -------
        Expression
            An `Expression` computing the resulting datetime. Returns `DateTime`.
        """
        return _datetime_subtract(date, period)

    @classmethod
    @include_in_docs
    def range(cls, start: DateTimeValue | None = None, end: DateTimeValue | None = None, periods: IntegerValue = 1, freq: Frequency = "D") -> Variable:
        """
        Generate a range of datetimes.

        Parameters
        ----------
        start: DateTimeValue | None
            The start datetime. If None, computes from end.
        end: DateTimeValue | None
            The end datetime (inclusive). If None, uses periods.
        periods: IntegerValue
            Number of periods to generate. Default: 1.
        freq: Frequency
            Frequency: "ms", "s", "m" (minute), "H", "D", "W", "M", "Y". Default: "D".

        Returns
        -------
        Variable
            A `Variable` representing the datetime range. Returns `DateTime`.

        Raises
        ------
        ValueError
            If neither start nor end is provided.
        """
        if start is None and end is None:
            raise ValueError("Invalid start/end date for datetime.range. Must provide at least start date or end date")
        # TODO - this transformation is currently LQP-focused. Eventually we will want to
        # move it into the LQP stack and have something general here.
        _milliseconds = {
            "ms": 1,
            "s": 1 / 1_000,
            "m": 1 / 60_000,
            "H": 1 / 3_600_000,
            "D": 1 / 86_400_000,
            "W": 1 / (86_400_000 * 7),
            "M": 1 / (86_400_000 * (365 / 12)),
            "Y": 1 / (86_400_000 * 365),
        }
        date_func = cls.add
        if start is None:
            start = end
            end = None
            date_func = cls.subtract
        assert start is not None
        if end is not None:
            num_ms = cls.period_milliseconds(start, end)
            if freq == "ms":
                _end = num_ms
            else:
                _end = math.ceil(num_ms * Float(_milliseconds[freq]))
            _end = core.cast(MetaRef(Integer), _end)
            # datetime_range is inclusive. add 1 since common.range is exclusive
            ix = common.range(0, _end + 1, 1)
        else:
            ix = common.range(0, periods, 1)
        _date = date_func(start, _periods[freq](ix))
        if isinstance(end, dt.datetime) :
            return select(_date).where(DateTime(end) >= _date)
        elif end is not None:
            return select(_date).where(end >= _date)
        return _date



    @classmethod
    @include_in_docs
    def period_milliseconds(cls, start: DateTimeValue, end: DateTimeValue) -> Expression:
        """
        Compute the number of milliseconds between two datetimes.

        Parameters
        ----------
        start: DateTimeValue
            The start datetime.
        end: DateTimeValue
            The end datetime.

        Returns
        -------
        Expression
            An Expression computing the number of milliseconds.
        """
        return _datetimes_period_milliseconds(start, end)

    @classmethod
    @include_in_docs
    def diff(cls, part: DateTimePart, start: DateTimeValue, end: DateTimeValue) -> Expression:
        """
        Calculate the difference between two datetimes in the specified datetime part units.

        Parameters
        ----------
        part: DateTimePart
            The unit to measure the difference in: "year", "quarter", "month", "week", "day",
            "hour", "minute", "second", or "millisecond".
        start: DateTimeValue
            The start datetime.
        end: DateTimeValue
            The end datetime.

        Returns
        -------
        Expression
            An `Expression` computing the difference. Returns `Integer`.

        Examples
        --------
        Calculate datetime differences:

        >>> datetime.datetime.diff("hour", Event.start_time, Event.end_time)
        >>> datetime.datetime.diff("minute", Task.created_at, Task.completed_at)
        >>> datetime.datetime.diff("second", Session.login_time, Session.logout_time)
        """
        return _datetime_diff(part, start, end)


#--------------------------------------------------
# Periods
#--------------------------------------------------

# Concepts
Nanoseconds = library.Type("Nanoseconds", [Integer])
Microseconds = library.Type("Microseconds", [Integer])
Milliseconds = library.Type("Milliseconds", [Integer])
Seconds = library.Type("Seconds", [Integer])
Minutes = library.Type("Minutes", [Integer])
Hours = library.Type("Hours", [Integer])
Days = library.Type("Days", [Integer])
Weeks = library.Type("Weeks", [Integer])
Months = library.Type("Months", [Integer])
Years = library.Type("Years", [Integer])

# Constructors from Date/DateTime
_dates_period_days = library.Relation("dates_period_days", [Field.input("start", Date), Field.input("end", Date), Field("days", Days)])
_datetimes_period_milliseconds = library.Relation("datetimes_period_milliseconds", [Field.input("start", DateTime), Field.input("end", DateTime), Field("milliseconds", Milliseconds)])

# Basic Constructors
_nanosecond = library.Relation("nanosecond", [Field.input("nanoseconds", Integer), Field("period", Nanoseconds)])
_microsecond = library.Relation("microsecond", [Field.input("microseconds", Integer), Field("period", Microseconds)])
_millisecond = library.Relation("millisecond", [Field.input("milliseconds", Integer), Field("period", Milliseconds)])
_second = library.Relation("second", [Field.input("seconds", Integer), Field("period", Seconds)])
_minute = library.Relation("minute", [Field.input("minutes", Integer), Field("period", Minutes)])
_hour = library.Relation("hour", [Field.input("hours", Integer), Field("period", Hours)])
_day = library.Relation("day", [Field.input("days", Integer), Field("period", Days)])
_week = library.Relation("week", [Field.input("weeks", Integer), Field("period", Weeks)])
_month = library.Relation("month", [Field.input("months", Integer), Field("period", Months)])
_year = library.Relation("year", [Field.input("years", Integer), Field("period", Years)])

@include_in_docs
def nanoseconds(period: IntegerValue) -> Expression:
    """
    Create a nanoseconds period.

    Parameters
    ----------
    period: IntegerValue
        The number of nanoseconds.

    Returns
    -------
    Expression
        An `Expression` computing the period.
    """
    return _nanosecond(period)

@include_in_docs
def microseconds(period: IntegerValue) -> Expression:
    """
    Create a microseconds period.

    Parameters
    ----------
    period: IntegerValue
        The number of microseconds.

    Returns
    -------
    Expression
        An `Expression` computing the period.
    """
    return _microsecond(period)

@include_in_docs
def milliseconds(period: IntegerValue) -> Expression:
    """
    Create a milliseconds period.

    Parameters
    ----------
    period: IntegerValue
        The number of milliseconds.

    Returns
    -------
    Expression
        An `Expression` computing the period.
    """
    return _millisecond(period)

@include_in_docs
def seconds(period: IntegerValue) -> Expression:
    """
    Create a seconds period.

    Parameters
    ----------
    period: IntegerValue
        The number of seconds.

    Returns
    -------
    Expression
        An `Expression` computing the period.
    """
    return _second(period)

@include_in_docs
def minutes(period: IntegerValue) -> Expression:
    """
    Create a minutes period.

    Parameters
    ----------
    period: IntegerValue
        The number of minutes.

    Returns
    -------
    Expression
        An `Expression` computing the period.
    """
    return _minute(period)

@include_in_docs
def hours(period: IntegerValue) -> Expression:
    """
    Create an hours period.

    Parameters
    ----------
    period: IntegerValue
        The number of hours.

    Returns
    -------
    Expression
        An `Expression` computing the period.

    Examples
    --------
    Create hour periods:

    >>> datetime.hours(24)
    >>> datetime.hours(Flight.duration_hours)
    """
    return _hour(period)

@include_in_docs
def days(period: IntegerValue) -> Expression:
    """
    Create a days period.

    Parameters
    ----------
    period: IntegerValue
        The number of days.

    Returns
    -------
    Expression
        An `Expression` computing the period.

    Examples
    --------
    Create day periods:

    >>> datetime.days(7)
    >>> datetime.days(Order.shipping_days)
    """
    return _day(period)

@include_in_docs
def weeks(period: IntegerValue) -> Expression:
    """
    Create a weeks period.

    Parameters
    ----------
    period: IntegerValue
        The number of weeks.

    Returns
    -------
    Expression
        An `Expression` computing the period.
    """
    return _week(period)

@include_in_docs
def months(period: IntegerValue) -> Expression:
    """
    Create a months period.

    Parameters
    ----------
    period: IntegerValue
        The number of months.

    Returns
    -------
    Expression
        An `Expression` computing the period.

    Examples
    --------
    Create month periods:

    >>> datetime.months(6)
    >>> datetime.months(Lease.duration_months)
    """
    return _month(period)

@include_in_docs
def years(period: IntegerValue) -> Expression:
    """
    Create a years period.

    Parameters
    ----------
    period: IntegerValue
        The number of years.

    Returns
    -------
    Expression
        An `Expression` computing the period.
    """
    return _year(period)

Frequency = Union[
    LiteralType["ms"],
    LiteralType["s"],
    LiteralType["m"],
    LiteralType["H"],
    LiteralType["D"],
    LiteralType["W"],
    LiteralType["M"],
    LiteralType["Y"],
]
"""
Type alias for time frequency strings used in date and datetime range operations.

Valid frequency strings:
- `"ms"`: milliseconds
- `"s"`: seconds
- `"m"`: minutes (lowercase m)
- `"H"`: hours
- `"D"`: days
- `"W"`: weeks
- `"M"`: months (uppercase M)
- `"Y"`: years

Examples
--------
Use frequency strings in date ranges:

>>> datetime.date.range(start_date, end_date, freq="D")  # Daily
>>> datetime.date.range(start_date, end_date, freq="W")  # Weekly
>>> datetime.date.range(start_date, end_date, freq="M")  # Monthly

Use frequency strings in datetime ranges:

>>> datetime.datetime.range(start_dt, end_dt, freq="H")   # Hourly
>>> datetime.datetime.range(start_dt, end_dt, freq="m")   # Every minute
>>> datetime.datetime.range(start_dt, end_dt, freq="ms")  # Milliseconds
"""

DatePart = Union[
    LiteralType["year"],
    LiteralType["month"],
    LiteralType["week"],
    LiteralType["day"],
]
"""
Type alias for date part strings used in date difference calculations.
"""

DateTimePart = Union[
    LiteralType["year"],
    LiteralType["month"],
    LiteralType["week"],
    LiteralType["day"],
    LiteralType["hour"],
    LiteralType["minute"],
    LiteralType["second"],
    LiteralType["millisecond"],
]
"""
Type alias for datetime part strings used in datetime difference calculations.
"""

_periods = {
    "ms": milliseconds,
    "s": seconds,
    "m": minutes,
    "H": hours,
    "D": days,
    "W": weeks,
    "M": months,
    "Y": years,
}

_days = {
    "D": 1,
    "W": Float(1/7),
    "M": Float(1/(365/12)),
    "Y": Float(1/365),
}

#--------------------------------------------------
# Helpers
#--------------------------------------------------

def _extract_tz(datetime: DateTimeValue, tz: dt.tzinfo|StringValue|None) -> StringValue:
    default_tz = "UTC"
    if tz is None:
        if isinstance(datetime, dt.datetime):
            tz = datetime.tzname() or default_tz
        else:
            tz = default_tz
    elif isinstance(tz, dt.tzinfo) :
        tz = tz.tzname(None) or default_tz
    return tz
