//! Extended temporal types supporting Neo4j's ±999,999,999 year range
//! with automatic promotion/demotion to chrono types when values fit.

use chrono::{DateTime as ChronoDateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt;

/// Maximum year supported (Neo4j compatibility)
pub const MAX_YEAR: i64 = 999_999_999;
/// Minimum year supported (Neo4j compatibility)
pub const MIN_YEAR: i64 = -999_999_999;

/// Chrono's exact year range (NaiveDate: -262143-01-01 to +262142-12-31)
pub const CHRONO_MAX_YEAR: i32 = 262_142;
pub const CHRONO_MIN_YEAR: i32 = -262_143;

/// Days from year 0 to 1970-01-01 (Unix epoch) in proleptic Gregorian calendar
const EPOCH_OFFSET: i128 = 719_528;

// ============================================================================
// ExtendedDate - Core extended date type
// ============================================================================

/// Extended date representation for years outside chrono's range
/// Supports proleptic Gregorian calendar from -999,999,999 to +999,999,999
///
/// # Indexing
/// For database indexing, use `to_ordinal_days()` which returns a unique i128
/// ordinal that preserves total ordering. This is O(1) and suitable for B-tree indexes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExtendedDate {
    year: i64,
    month: u8,  // 1-12
    day: u8,    // 1-31
}

impl ExtendedDate {
    /// Create a new extended date with validation
    pub fn from_ymd(year: i64, month: u32, day: u32) -> Option<Self> {
        if !(MIN_YEAR..=MAX_YEAR).contains(&year) {
            return None;
        }
        if !(1..=12).contains(&month) {
            return None;
        }
        let max_day = Self::days_in_month(year, month);
        if day < 1 || day > max_day {
            return None;
        }
        Some(Self {
            year,
            month: month as u8,
            day: day as u8,
        })
    }

    /// Convert to chrono NaiveDate if within range
    pub fn to_chrono(&self) -> Option<NaiveDate> {
        if self.is_in_chrono_range() {
            NaiveDate::from_ymd_opt(self.year as i32, self.month as u32, self.day as u32)
        } else {
            None
        }
    }

    /// Create from chrono NaiveDate
    pub fn from_chrono(date: NaiveDate) -> Self {
        Self {
            year: date.year() as i64,
            month: date.month() as u8,
            day: date.day() as u8,
        }
    }

    /// Check if this date can be represented by chrono
    pub fn is_in_chrono_range(&self) -> bool {
        self.year >= CHRONO_MIN_YEAR as i64 && self.year <= CHRONO_MAX_YEAR as i64
    }

    // Accessors
    pub fn year(&self) -> i64 { self.year }
    pub fn month(&self) -> u32 { self.month as u32 }
    pub fn day(&self) -> u32 { self.day as u32 }

    /// Proleptic Gregorian leap year calculation
    pub fn is_leap_year(year: i64) -> bool {
        (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
    }

    /// Days in a given month
    pub fn days_in_month(year: i64, month: u32) -> u32 {
        match month {
            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
            4 | 6 | 9 | 11 => 30,
            2 => if Self::is_leap_year(year) { 29 } else { 28 },
            _ => 0,
        }
    }

    /// Days in a year
    pub fn days_in_year(year: i64) -> i64 {
        if Self::is_leap_year(year) { 366 } else { 365 }
    }

    /// Convert to epoch days (days since 1970-01-01)
    /// This is an O(1) calculation suitable for indexing.
    /// Returns i128 to handle the full range without overflow.
    pub fn to_epoch_days(&self) -> i128 {
        let y = self.year as i128;
        let m = self.month as i128;
        let d = self.day as i128;

        // Adjust for months January and February (treat as months 13, 14 of previous year)
        let (y_adj, m_adj) = if m <= 2 {
            (y - 1, m + 12)
        } else {
            (y, m)
        };

        // Days from year 0 using proleptic Gregorian calendar formula
        // This formula computes days from a reference point
        let leap_days = if y_adj >= 0 {
            y_adj / 4 - y_adj / 100 + y_adj / 400
        } else {
            // For negative years, use floor division behavior
            (y_adj - 3) / 4 - (y_adj - 99) / 100 + (y_adj - 399) / 400
        };

        365 * y_adj + leap_days + (153 * (m_adj - 3) + 2) / 5 + d - EPOCH_OFFSET
    }

    /// Convert to ordinal days from epoch (year 1, day 1 = day 1)
    /// Returns i128 to handle the full range without overflow.
    /// This is an O(1) calculation suitable for database indexing.
    pub fn to_ordinal_days(&self) -> i128 {
        // Ordinal days where year 1, day 1 = 1
        // Relationship: ordinal_days = epoch_days + days_from_epoch_to_year1_day1
        // The epoch is 1970-01-01, and year 1 day 1 is 0001-01-01
        // Days from 0001-01-01 to 1970-01-01 = EPOCH_OFFSET - 1 (since year 1 day 1 = day 1)
        self.to_epoch_days() + EPOCH_OFFSET
    }

    /// Create from epoch days (days since 1970-01-01)
    pub fn from_epoch_days(days: i128) -> Option<Self> {
        // Algorithm from Howard Hinnant's date algorithms
        // http://howardhinnant.github.io/date_algorithms.html
        let z = days + 719468; // Days since March 1, year 0

        // Compute era (400-year period)
        let era = if z >= 0 { z / 146097 } else { (z - 146096) / 146097 };
        let doe = z - era * 146097; // Day of era [0, 146096]
        let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // Year of era [0, 399]
        let y = yoe + era * 400;
        let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // Day of year [0, 365]
        let mp = (5 * doy + 2) / 153; // Month position [0, 11]
        let d = doy - (153 * mp + 2) / 5 + 1; // Day [1, 31]
        let m = if mp < 10 { mp + 3 } else { mp - 9 }; // Month [1, 12]
        let y = if m <= 2 { y + 1 } else { y };

        // Check range
        if y < MIN_YEAR as i128 || y > MAX_YEAR as i128 {
            return None;
        }

        Self::from_ymd(y as i64, m as u32, d as u32)
    }

    /// Create from ordinal days since epoch
    pub fn from_ordinal_days(days: i128) -> Option<Self> {
        // Convert ordinal days (year 1 day 1 = 1) to epoch days
        Self::from_epoch_days(days - EPOCH_OFFSET)
    }

    /// Add days to this date
    pub fn add_days(&self, days: i64) -> Option<Self> {
        let ordinal = self.to_ordinal_days() + days as i128;
        Self::from_ordinal_days(ordinal)
    }

    /// Add months to this date (calendar arithmetic)
    pub fn add_months(&self, months: i64) -> Option<Self> {
        let total_months = (self.year * 12 + self.month as i64 - 1) + months;
        let new_year = total_months.div_euclid(12);
        let new_month = (total_months.rem_euclid(12) + 1) as u32;

        // Clamp day to valid range for new month
        let max_day = Self::days_in_month(new_year, new_month);
        let new_day = self.day.min(max_day as u8) as u32;

        Self::from_ymd(new_year, new_month, new_day)
    }
}

impl PartialOrd for ExtendedDate {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for ExtendedDate {
    fn cmp(&self, other: &Self) -> Ordering {
        self.year.cmp(&other.year)
            .then(self.month.cmp(&other.month))
            .then(self.day.cmp(&other.day))
    }
}

impl fmt::Display for ExtendedDate {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.year >= 0 && self.year <= 9999 {
            write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
        } else if self.year > 0 {
            write!(f, "+{}-{:02}-{:02}", self.year, self.month, self.day)
        } else {
            write!(f, "{}-{:02}-{:02}", self.year, self.month, self.day)
        }
    }
}

// ============================================================================
// NxDate - Unified date type with automatic promotion/demotion
// ============================================================================

/// Unified date type supporting both standard (chrono) and extended ranges
/// Automatically promotes to Extended when operations exceed chrono range
/// Automatically demotes to Standard when results fit in chrono range
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NxDate {
    /// Standard date using chrono (year range: ±262K)
    Standard(NaiveDate),
    /// Extended date for large years (year range: ±999M)
    Extended(ExtendedDate),
}

impl NxDate {
    /// Smart constructor - uses Standard if year fits in chrono range
    pub fn from_ymd(year: i64, month: u32, day: u32) -> Option<Self> {
        if year >= CHRONO_MIN_YEAR as i64 && year <= CHRONO_MAX_YEAR as i64 {
            NaiveDate::from_ymd_opt(year as i32, month, day)
                .map(NxDate::Standard)
        } else {
            ExtendedDate::from_ymd(year, month, day)
                .map(NxDate::Extended)
        }
    }

    /// Convert to chrono NaiveDate if in standard range
    pub fn to_chrono(&self) -> Option<NaiveDate> {
        match self {
            NxDate::Standard(d) => Some(*d),
            NxDate::Extended(d) => d.to_chrono(),
        }
    }

    /// Check if this is a standard (chrono) date
    pub fn is_standard(&self) -> bool {
        matches!(self, NxDate::Standard(_))
    }

    /// Get year (works for both variants)
    pub fn year(&self) -> i64 {
        match self {
            NxDate::Standard(d) => d.year() as i64,
            NxDate::Extended(d) => d.year(),
        }
    }

    /// Get month (works for both variants)
    pub fn month(&self) -> u32 {
        match self {
            NxDate::Standard(d) => d.month(),
            NxDate::Extended(d) => d.month(),
        }
    }

    /// Get day (works for both variants)
    pub fn day(&self) -> u32 {
        match self {
            NxDate::Standard(d) => d.day(),
            NxDate::Extended(d) => d.day(),
        }
    }

    /// Convert to ordinal days (for arithmetic)
    pub fn to_ordinal_days(&self) -> i128 {
        match self {
            NxDate::Standard(d) => ExtendedDate::from_chrono(*d).to_ordinal_days(),
            NxDate::Extended(d) => d.to_ordinal_days(),
        }
    }

    /// Create from ordinal days with automatic variant selection
    pub fn from_ordinal_days(days: i128) -> Option<Self> {
        let ext = ExtendedDate::from_ordinal_days(days)?;
        if ext.is_in_chrono_range() {
            Some(NxDate::Standard(ext.to_chrono().unwrap()))
        } else {
            Some(NxDate::Extended(ext))
        }
    }

    /// Add days with automatic promotion/demotion
    pub fn add_days(&self, days: i64) -> Option<Self> {
        let ordinal = self.to_ordinal_days() + days as i128;
        Self::from_ordinal_days(ordinal)
    }

    /// Add months with automatic promotion/demotion
    pub fn add_months(&self, months: i64) -> Option<Self> {
        let ext = match self {
            NxDate::Standard(d) => ExtendedDate::from_chrono(*d),
            NxDate::Extended(d) => *d,
        };
        let result = ext.add_months(months)?;
        if result.is_in_chrono_range() {
            Some(NxDate::Standard(result.to_chrono().unwrap()))
        } else {
            Some(NxDate::Extended(result))
        }
    }
}

impl From<NaiveDate> for NxDate {
    fn from(date: NaiveDate) -> Self {
        NxDate::Standard(date)
    }
}

impl PartialOrd for NxDate {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for NxDate {
    fn cmp(&self, other: &Self) -> Ordering {
        // Compare via ordinal days for consistency across variants
        self.to_ordinal_days().cmp(&other.to_ordinal_days())
    }
}

impl fmt::Display for NxDate {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            NxDate::Standard(d) => write!(f, "{}", d.format("%Y-%m-%d")),
            NxDate::Extended(d) => write!(f, "{}", d),
        }
    }
}

// ============================================================================
// ExtendedLocalDateTime - DateTime without timezone for extended range
// ============================================================================

/// Extended local datetime representation for years outside chrono's range
/// Combines ExtendedDate with NaiveTime (time always fits in chrono)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExtendedLocalDateTime {
    pub date: ExtendedDate,
    pub time: NaiveTime,
}

impl ExtendedLocalDateTime {
    /// Create a new extended local datetime
    pub fn new(date: ExtendedDate, time: NaiveTime) -> Self {
        Self { date, time }
    }

    /// Create from components
    pub fn from_ymdhms(
        year: i64,
        month: u32,
        day: u32,
        hour: u32,
        minute: u32,
        second: u32,
    ) -> Option<Self> {
        let date = ExtendedDate::from_ymd(year, month, day)?;
        let time = NaiveTime::from_hms_opt(hour, minute, second)?;
        Some(Self { date, time })
    }

    /// Create from components with nanoseconds
    pub fn from_ymdhms_nano(
        year: i64,
        month: u32,
        day: u32,
        hour: u32,
        minute: u32,
        second: u32,
        nano: u32,
    ) -> Option<Self> {
        let date = ExtendedDate::from_ymd(year, month, day)?;
        let time = NaiveTime::from_hms_nano_opt(hour, minute, second, nano)?;
        Some(Self { date, time })
    }

    /// Convert to chrono NaiveDateTime if within range
    pub fn to_chrono(&self) -> Option<NaiveDateTime> {
        let naive_date = self.date.to_chrono()?;
        Some(NaiveDateTime::new(naive_date, self.time))
    }

    /// Create from chrono NaiveDateTime
    pub fn from_chrono(dt: NaiveDateTime) -> Self {
        Self {
            date: ExtendedDate::from_chrono(dt.date()),
            time: dt.time(),
        }
    }

    /// Check if this datetime can be represented by chrono
    pub fn is_in_chrono_range(&self) -> bool {
        self.date.is_in_chrono_range()
    }

    // Accessors
    pub fn year(&self) -> i64 { self.date.year() }
    pub fn month(&self) -> u32 { self.date.month() }
    pub fn day(&self) -> u32 { self.date.day() }
    pub fn hour(&self) -> u32 { self.time.hour() }
    pub fn minute(&self) -> u32 { self.time.minute() }
    pub fn second(&self) -> u32 { self.time.second() }
    pub fn nanosecond(&self) -> u32 { self.time.nanosecond() }

    /// Convert to epoch seconds (seconds since 1970-01-01T00:00:00)
    pub fn to_epoch_seconds(&self) -> i128 {
        let days = self.date.to_epoch_days();
        let time_secs = self.time.num_seconds_from_midnight() as i128;
        days * 86400 + time_secs
    }

    /// Convert to epoch nanoseconds for precise indexing
    pub fn to_epoch_nanos(&self) -> i128 {
        let secs = self.to_epoch_seconds();
        secs * 1_000_000_000 + self.time.nanosecond() as i128
    }

    /// Add days to this datetime
    pub fn add_days(&self, days: i64) -> Option<Self> {
        let new_date = self.date.add_days(days)?;
        Some(Self { date: new_date, time: self.time })
    }

    /// Add months to this datetime
    pub fn add_months(&self, months: i64) -> Option<Self> {
        let new_date = self.date.add_months(months)?;
        Some(Self { date: new_date, time: self.time })
    }
}

impl PartialOrd for ExtendedLocalDateTime {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for ExtendedLocalDateTime {
    fn cmp(&self, other: &Self) -> Ordering {
        self.date.cmp(&other.date)
            .then(self.time.cmp(&other.time))
    }
}

impl fmt::Display for ExtendedLocalDateTime {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let nanos = self.time.nanosecond();
        if nanos == 0 {
            write!(f, "{}T{}", self.date, self.time.format("%H:%M:%S"))
        } else {
            // Format nanoseconds, trimming trailing zeros
            let frac = format!("{:09}", nanos);
            let frac = frac.trim_end_matches('0');
            write!(f, "{}T{}.{}", self.date, self.time.format("%H:%M:%S"), frac)
        }
    }
}

// ============================================================================
// NxLocalDateTime - Unified local datetime with automatic promotion/demotion
// ============================================================================

/// Unified local datetime type supporting both standard (chrono) and extended ranges
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NxLocalDateTime {
    /// Standard datetime using chrono (year range: ±262K)
    Standard(NaiveDateTime),
    /// Extended datetime for large years (year range: ±999M)
    Extended(ExtendedLocalDateTime),
}

impl NxLocalDateTime {
    /// Smart constructor - uses Standard if year fits in chrono range
    pub fn from_ymdhms(
        year: i64,
        month: u32,
        day: u32,
        hour: u32,
        minute: u32,
        second: u32,
    ) -> Option<Self> {
        if year >= CHRONO_MIN_YEAR as i64 && year <= CHRONO_MAX_YEAR as i64 {
            let date = NaiveDate::from_ymd_opt(year as i32, month, day)?;
            let time = NaiveTime::from_hms_opt(hour, minute, second)?;
            Some(NxLocalDateTime::Standard(NaiveDateTime::new(date, time)))
        } else {
            ExtendedLocalDateTime::from_ymdhms(year, month, day, hour, minute, second)
                .map(NxLocalDateTime::Extended)
        }
    }

    /// Smart constructor with nanoseconds
    pub fn from_ymdhms_nano(
        year: i64,
        month: u32,
        day: u32,
        hour: u32,
        minute: u32,
        second: u32,
        nano: u32,
    ) -> Option<Self> {
        if year >= CHRONO_MIN_YEAR as i64 && year <= CHRONO_MAX_YEAR as i64 {
            let date = NaiveDate::from_ymd_opt(year as i32, month, day)?;
            let time = NaiveTime::from_hms_nano_opt(hour, minute, second, nano)?;
            Some(NxLocalDateTime::Standard(NaiveDateTime::new(date, time)))
        } else {
            ExtendedLocalDateTime::from_ymdhms_nano(year, month, day, hour, minute, second, nano)
                .map(NxLocalDateTime::Extended)
        }
    }

    /// Create from NxDate and NaiveTime
    pub fn from_date_time(date: NxDate, time: NaiveTime) -> Self {
        match date {
            NxDate::Standard(d) => NxLocalDateTime::Standard(NaiveDateTime::new(d, time)),
            NxDate::Extended(d) => NxLocalDateTime::Extended(ExtendedLocalDateTime::new(d, time)),
        }
    }

    /// Convert to chrono NaiveDateTime if in standard range
    pub fn to_chrono(&self) -> Option<NaiveDateTime> {
        match self {
            NxLocalDateTime::Standard(dt) => Some(*dt),
            NxLocalDateTime::Extended(dt) => dt.to_chrono(),
        }
    }

    /// Check if this is a standard (chrono) datetime
    pub fn is_standard(&self) -> bool {
        matches!(self, NxLocalDateTime::Standard(_))
    }

    // Accessors
    pub fn year(&self) -> i64 {
        match self {
            NxLocalDateTime::Standard(dt) => dt.year() as i64,
            NxLocalDateTime::Extended(dt) => dt.year(),
        }
    }

    pub fn month(&self) -> u32 {
        match self {
            NxLocalDateTime::Standard(dt) => dt.month(),
            NxLocalDateTime::Extended(dt) => dt.month(),
        }
    }

    pub fn day(&self) -> u32 {
        match self {
            NxLocalDateTime::Standard(dt) => dt.day(),
            NxLocalDateTime::Extended(dt) => dt.day(),
        }
    }

    pub fn hour(&self) -> u32 {
        match self {
            NxLocalDateTime::Standard(dt) => dt.hour(),
            NxLocalDateTime::Extended(dt) => dt.hour(),
        }
    }

    pub fn minute(&self) -> u32 {
        match self {
            NxLocalDateTime::Standard(dt) => dt.minute(),
            NxLocalDateTime::Extended(dt) => dt.minute(),
        }
    }

    pub fn second(&self) -> u32 {
        match self {
            NxLocalDateTime::Standard(dt) => dt.second(),
            NxLocalDateTime::Extended(dt) => dt.second(),
        }
    }

    pub fn nanosecond(&self) -> u32 {
        match self {
            NxLocalDateTime::Standard(dt) => dt.nanosecond(),
            NxLocalDateTime::Extended(dt) => dt.nanosecond(),
        }
    }

    /// Get the date part
    pub fn date(&self) -> NxDate {
        match self {
            NxLocalDateTime::Standard(dt) => NxDate::Standard(dt.date()),
            NxLocalDateTime::Extended(dt) => NxDate::Extended(dt.date),
        }
    }

    /// Get the time part
    pub fn time(&self) -> NaiveTime {
        match self {
            NxLocalDateTime::Standard(dt) => dt.time(),
            NxLocalDateTime::Extended(dt) => dt.time,
        }
    }

    /// Convert to epoch nanoseconds for indexing
    pub fn to_epoch_nanos(&self) -> i128 {
        match self {
            NxLocalDateTime::Standard(dt) => {
                let secs = dt.and_utc().timestamp() as i128;
                secs * 1_000_000_000 + dt.nanosecond() as i128
            }
            NxLocalDateTime::Extended(dt) => dt.to_epoch_nanos(),
        }
    }

    /// Add days with automatic promotion/demotion
    pub fn add_days(&self, days: i64) -> Option<Self> {
        match self {
            NxLocalDateTime::Standard(dt) => {
                // Try chrono first
                let result = *dt + chrono::Duration::days(days);
                let year = result.year() as i64;
                if year >= CHRONO_MIN_YEAR as i64 && year <= CHRONO_MAX_YEAR as i64 {
                    Some(NxLocalDateTime::Standard(result))
                } else {
                    // Promote to extended
                    let ext = ExtendedLocalDateTime::from_chrono(*dt);
                    ext.add_days(days).map(NxLocalDateTime::Extended)
                }
            }
            NxLocalDateTime::Extended(dt) => {
                let result = dt.add_days(days)?;
                if result.is_in_chrono_range() {
                    Some(NxLocalDateTime::Standard(result.to_chrono().unwrap()))
                } else {
                    Some(NxLocalDateTime::Extended(result))
                }
            }
        }
    }

    /// Add months with automatic promotion/demotion
    pub fn add_months(&self, months: i64) -> Option<Self> {
        let ext = match self {
            NxLocalDateTime::Standard(dt) => ExtendedLocalDateTime::from_chrono(*dt),
            NxLocalDateTime::Extended(dt) => *dt,
        };
        let result = ext.add_months(months)?;
        if result.is_in_chrono_range() {
            Some(NxLocalDateTime::Standard(result.to_chrono().unwrap()))
        } else {
            Some(NxLocalDateTime::Extended(result))
        }
    }
}

impl From<NaiveDateTime> for NxLocalDateTime {
    fn from(dt: NaiveDateTime) -> Self {
        NxLocalDateTime::Standard(dt)
    }
}

impl PartialOrd for NxLocalDateTime {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for NxLocalDateTime {
    fn cmp(&self, other: &Self) -> Ordering {
        self.to_epoch_nanos().cmp(&other.to_epoch_nanos())
    }
}

impl fmt::Display for NxLocalDateTime {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            NxLocalDateTime::Standard(dt) => {
                let nanos = dt.nanosecond();
                if nanos == 0 {
                    write!(f, "{}", dt.format("%Y-%m-%dT%H:%M:%S"))
                } else {
                    let frac = format!("{:09}", nanos);
                    let frac = frac.trim_end_matches('0');
                    write!(f, "{}.{}", dt.format("%Y-%m-%dT%H:%M:%S"), frac)
                }
            }
            NxLocalDateTime::Extended(dt) => write!(f, "{}", dt),
        }
    }
}

// ============================================================================
// ExtendedDateTime - DateTime with timezone for extended range
// ============================================================================

/// Extended datetime with timezone for years outside chrono's range
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExtendedDateTime {
    pub local_datetime: ExtendedLocalDateTime,
    pub offset: FixedOffset,
}

// Custom serialization for ExtendedDateTime since FixedOffset doesn't impl Serialize
impl Serialize for ExtendedDateTime {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        use serde::ser::SerializeStruct;
        let mut state = serializer.serialize_struct("ExtendedDateTime", 2)?;
        state.serialize_field("local_datetime", &self.local_datetime)?;
        state.serialize_field("offset_seconds", &self.offset.local_minus_utc())?;
        state.end()
    }
}

impl<'de> Deserialize<'de> for ExtendedDateTime {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct Helper {
            local_datetime: ExtendedLocalDateTime,
            offset_seconds: i32,
        }
        let helper = Helper::deserialize(deserializer)?;
        let offset = FixedOffset::east_opt(helper.offset_seconds)
            .ok_or_else(|| serde::de::Error::custom("Invalid timezone offset"))?;
        Ok(ExtendedDateTime {
            local_datetime: helper.local_datetime,
            offset,
        })
    }
}

impl ExtendedDateTime {
    /// Create a new extended datetime with timezone
    pub fn new(local_datetime: ExtendedLocalDateTime, offset: FixedOffset) -> Self {
        Self { local_datetime, offset }
    }

    /// Convert to chrono `DateTime<FixedOffset>` if within range
    pub fn to_chrono(&self) -> Option<ChronoDateTime<FixedOffset>> {
        let naive = self.local_datetime.to_chrono()?;
        self.offset.from_local_datetime(&naive).single()
    }

    /// Create from chrono `DateTime<FixedOffset>`
    pub fn from_chrono(dt: ChronoDateTime<FixedOffset>) -> Self {
        Self {
            local_datetime: ExtendedLocalDateTime::from_chrono(dt.naive_local()),
            offset: *dt.offset(),
        }
    }

    /// Check if this datetime can be represented by chrono
    pub fn is_in_chrono_range(&self) -> bool {
        self.local_datetime.is_in_chrono_range()
    }

    // Accessors delegating to local_datetime
    pub fn year(&self) -> i64 { self.local_datetime.year() }
    pub fn month(&self) -> u32 { self.local_datetime.month() }
    pub fn day(&self) -> u32 { self.local_datetime.day() }
    pub fn hour(&self) -> u32 { self.local_datetime.hour() }
    pub fn minute(&self) -> u32 { self.local_datetime.minute() }
    pub fn second(&self) -> u32 { self.local_datetime.second() }
    pub fn nanosecond(&self) -> u32 { self.local_datetime.nanosecond() }

    pub fn timezone(&self) -> String {
        self.offset.to_string()
    }

    /// Convert to UTC epoch nanoseconds for indexing
    pub fn to_epoch_nanos_utc(&self) -> i128 {
        let local_nanos = self.local_datetime.to_epoch_nanos();
        let offset_nanos = self.offset.local_minus_utc() as i128 * 1_000_000_000;
        local_nanos - offset_nanos
    }
}

impl PartialOrd for ExtendedDateTime {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for ExtendedDateTime {
    fn cmp(&self, other: &Self) -> Ordering {
        // Compare in UTC
        self.to_epoch_nanos_utc().cmp(&other.to_epoch_nanos_utc())
    }
}

impl fmt::Display for ExtendedDateTime {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let offset_secs = self.offset.local_minus_utc();
        let offset_str = if offset_secs == 0 {
            "Z".to_string()
        } else {
            self.offset.to_string()
        };
        write!(f, "{}{}", self.local_datetime, offset_str)
    }
}

// ============================================================================
// NxDateTime - Unified datetime with timezone and automatic promotion/demotion
// ============================================================================

/// Unified datetime with timezone supporting both standard and extended ranges
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NxDateTime {
    /// Standard datetime using chrono (year range: ±262K)
    Standard(ChronoDateTime<FixedOffset>),
    /// Extended datetime for large years (year range: ±999M)
    Extended(ExtendedDateTime),
}

impl NxDateTime {
    /// Convert to chrono `DateTime<FixedOffset>` if in standard range
    pub fn to_chrono(&self) -> Option<ChronoDateTime<FixedOffset>> {
        match self {
            NxDateTime::Standard(dt) => Some(*dt),
            NxDateTime::Extended(dt) => dt.to_chrono(),
        }
    }

    /// Check if this is a standard (chrono) datetime
    pub fn is_standard(&self) -> bool {
        matches!(self, NxDateTime::Standard(_))
    }

    // Accessors
    pub fn year(&self) -> i64 {
        match self {
            NxDateTime::Standard(dt) => dt.year() as i64,
            NxDateTime::Extended(dt) => dt.year(),
        }
    }

    pub fn month(&self) -> u32 {
        match self {
            NxDateTime::Standard(dt) => dt.month(),
            NxDateTime::Extended(dt) => dt.month(),
        }
    }

    pub fn day(&self) -> u32 {
        match self {
            NxDateTime::Standard(dt) => dt.day(),
            NxDateTime::Extended(dt) => dt.day(),
        }
    }

    pub fn hour(&self) -> u32 {
        match self {
            NxDateTime::Standard(dt) => dt.hour(),
            NxDateTime::Extended(dt) => dt.hour(),
        }
    }

    pub fn minute(&self) -> u32 {
        match self {
            NxDateTime::Standard(dt) => dt.minute(),
            NxDateTime::Extended(dt) => dt.minute(),
        }
    }

    pub fn second(&self) -> u32 {
        match self {
            NxDateTime::Standard(dt) => dt.second(),
            NxDateTime::Extended(dt) => dt.second(),
        }
    }

    pub fn nanosecond(&self) -> u32 {
        match self {
            NxDateTime::Standard(dt) => dt.nanosecond(),
            NxDateTime::Extended(dt) => dt.nanosecond(),
        }
    }

    pub fn timezone(&self) -> String {
        match self {
            NxDateTime::Standard(dt) => dt.offset().to_string(),
            NxDateTime::Extended(dt) => dt.timezone(),
        }
    }

    /// Convert to UTC epoch nanoseconds for indexing
    pub fn to_epoch_nanos_utc(&self) -> i128 {
        match self {
            NxDateTime::Standard(dt) => {
                let secs = dt.timestamp() as i128;
                secs * 1_000_000_000 + dt.nanosecond() as i128
            }
            NxDateTime::Extended(dt) => dt.to_epoch_nanos_utc(),
        }
    }
}

impl From<ChronoDateTime<FixedOffset>> for NxDateTime {
    fn from(dt: ChronoDateTime<FixedOffset>) -> Self {
        NxDateTime::Standard(dt)
    }
}

impl PartialOrd for NxDateTime {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for NxDateTime {
    fn cmp(&self, other: &Self) -> Ordering {
        self.to_epoch_nanos_utc().cmp(&other.to_epoch_nanos_utc())
    }
}

impl fmt::Display for NxDateTime {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            NxDateTime::Standard(dt) => {
                let nanos = dt.nanosecond();
                let offset_secs = dt.offset().local_minus_utc();
                let offset_str = if offset_secs == 0 {
                    "Z".to_string()
                } else {
                    dt.offset().to_string()
                };
                if nanos == 0 {
                    write!(f, "{}{}", dt.format("%Y-%m-%dT%H:%M:%S"), offset_str)
                } else {
                    let frac = format!("{:09}", nanos);
                    let frac = frac.trim_end_matches('0');
                    write!(f, "{}.{}{}", dt.format("%Y-%m-%dT%H:%M:%S"), frac, offset_str)
                }
            }
            NxDateTime::Extended(dt) => write!(f, "{}", dt),
        }
    }
}

// ============================================================================
// Serde implementations for new enums
// ============================================================================

impl Serialize for NxDate {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        // Serialize as string representation
        serializer.serialize_str(&self.to_string())
    }
}

/// Helper function to parse NxDate from string
fn parse_nxdate_string(s: &str) -> Result<NxDate, String> {
    // First try standard format
    if let Ok(date) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
        return Ok(NxDate::Standard(date));
    }
    // Try extended format with sign prefix
    if s.starts_with('+') || s.starts_with('-') {
        let sign = if s.starts_with('-') { -1i64 } else { 1i64 };
        let rest = &s[1..];
        if let Some(dash) = rest.find('-') {
            let year_str = &rest[..dash];
            let remainder = &rest[dash + 1..];
            if let Ok(year_abs) = year_str.parse::<i64>() {
                let year = sign * year_abs;
                let parts: Vec<&str> = remainder.split('-').collect();
                if parts.len() == 2 {
                    if let (Ok(month), Ok(day)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
                        if let Some(date) = NxDate::from_ymd(year, month, day) {
                            return Ok(date);
                        }
                    }
                }
            }
        }
    }
    Err(format!("Invalid date: {}", s))
}

impl<'de> Deserialize<'de> for NxDate {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        parse_nxdate_string(&s).map_err(serde::de::Error::custom)
    }
}

impl Serialize for NxLocalDateTime {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> Deserialize<'de> for NxLocalDateTime {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        // Try standard format
        if let Ok(dt) = NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S%.f") {
            return Ok(NxLocalDateTime::Standard(dt));
        }
        if let Ok(dt) = NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S") {
            return Ok(NxLocalDateTime::Standard(dt));
        }
        // For extended format, parse date and time separately
        if let Some(t_pos) = s.find('T') {
            let date_str = &s[..t_pos];
            let time_str = &s[t_pos + 1..];

            // Parse time
            let time = NaiveTime::parse_from_str(time_str, "%H:%M:%S%.f")
                .or_else(|_| NaiveTime::parse_from_str(time_str, "%H:%M:%S"))
                .map_err(|_| serde::de::Error::custom(format!("Invalid time: {}", time_str)))?;

            // Parse date (may be extended) using our NxDate parsing logic
            let date = parse_nxdate_string(date_str)
                .map_err(serde::de::Error::custom)?;
            return Ok(NxLocalDateTime::from_date_time(date, time));
        }
        Err(serde::de::Error::custom(format!("Invalid datetime: {}", s)))
    }
}

impl Serialize for NxDateTime {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> Deserialize<'de> for NxDateTime {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        // Try standard RFC3339 format
        if let Ok(dt) = ChronoDateTime::parse_from_rfc3339(&s) {
            return Ok(NxDateTime::Standard(dt));
        }
        // For extended format, would need more complex parsing
        Err(serde::de::Error::custom(format!("Invalid datetime with timezone: {}", s)))
    }
}

// ============================================================================
// Tests
// ============================================================================

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

    #[test]
    fn test_standard_date_creation() {
        let date = NxDate::from_ymd(2024, 6, 15).unwrap();
        assert!(date.is_standard());
        assert_eq!(date.year(), 2024);
        assert_eq!(date.month(), 6);
        assert_eq!(date.day(), 15);
        assert!(date.to_chrono().is_some());
    }

    #[test]
    fn test_extended_date_creation() {
        let date = NxDate::from_ymd(500_000_000, 6, 15).unwrap();
        assert!(!date.is_standard());
        assert_eq!(date.year(), 500_000_000);
        assert!(date.to_chrono().is_none()); // Too big for chrono
    }

    #[test]
    fn test_automatic_promotion() {
        // Start near chrono limit
        let date = NxDate::from_ymd(262_000, 1, 1).unwrap();
        assert!(date.is_standard());

        // Add enough days to exceed chrono range
        let result = date.add_days(365 * 1000).unwrap(); // +1000 years
        assert!(!result.is_standard()); // Promoted to Extended
        assert!(result.year() > 262_143); // Beyond chrono
    }

    #[test]
    fn test_automatic_demotion() {
        // Start with extended date
        let date = NxDate::from_ymd(300_000, 1, 1).unwrap();
        assert!(!date.is_standard());

        // Subtract enough to fit in chrono range
        let result = date.add_days(-365 * 50_000).unwrap(); // -50K years
        assert!(result.is_standard()); // Demoted to Standard
        assert!(result.to_chrono().is_some()); // Can get chrono date
    }

    #[test]
    fn test_mixed_comparison() {
        let standard = NxDate::from_ymd(2024, 1, 1).unwrap();
        let extended = NxDate::from_ymd(500_000_000, 1, 1).unwrap();

        assert!(extended > standard);
        assert!(standard < extended);
    }

    #[test]
    fn test_negative_years() {
        let ancient = NxDate::from_ymd(-500_000_000, 6, 15).unwrap();
        let modern = NxDate::from_ymd(2024, 1, 1).unwrap();

        assert!(modern > ancient);
        assert_eq!(ancient.year(), -500_000_000);
    }

    #[test]
    fn test_leap_year() {
        assert!(ExtendedDate::is_leap_year(2000));
        assert!(!ExtendedDate::is_leap_year(1900));
        assert!(ExtendedDate::is_leap_year(2024));
        assert!(!ExtendedDate::is_leap_year(2023));

        // Works for extreme years too
        assert!(ExtendedDate::is_leap_year(500_000_000)); // divisible by 400
    }

    #[test]
    fn test_add_months() {
        let date = NxDate::from_ymd(2024, 1, 31).unwrap();
        let result = date.add_months(1).unwrap(); // Jan 31 + 1 month
        assert_eq!(result.month(), 2);
        assert_eq!(result.day(), 29); // 2024 is leap year, clamped to Feb 29
    }

    #[test]
    fn test_display_format() {
        assert_eq!(NxDate::from_ymd(2024, 6, 15).unwrap().to_string(), "2024-06-15");
        assert_eq!(NxDate::from_ymd(500_000_000, 1, 1).unwrap().to_string(), "+500000000-01-01");
        assert_eq!(NxDate::from_ymd(-500_000_000, 12, 31).unwrap().to_string(), "-500000000-12-31");
    }

    #[test]
    fn test_duration_between_dates() {
        let d1 = NxDate::from_ymd(2024, 1, 1).unwrap();
        let d2 = NxDate::from_ymd(2025, 1, 1).unwrap();

        let diff = d2.to_ordinal_days() - d1.to_ordinal_days();
        assert_eq!(diff, 366); // 2024 is a leap year
    }

    #[test]
    fn test_extreme_range() {
        // Maximum supported date
        let max = NxDate::from_ymd(999_999_999, 12, 31).unwrap();
        assert_eq!(max.year(), 999_999_999);

        // Minimum supported date
        let min = NxDate::from_ymd(-999_999_999, 1, 1).unwrap();
        assert_eq!(min.year(), -999_999_999);

        // Beyond range should fail
        assert!(NxDate::from_ymd(1_000_000_000, 1, 1).is_none());
    }
}
