use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Weekday};

use crate::parser::CronExpr;

/// Represents a parsed and ready-to-use cron schedule
#[derive(Debug, Clone)]
pub struct CronSchedule {
    expr: CronExpr,
    day_or: bool,
    implement_cron_bug: bool,
}

impl CronSchedule {
    /// Parse a cron expression and create a schedule
    pub fn parse(
        expr: &str,
        hash_id: Option<&str>,
        second_at_beginning: bool,
        day_or: bool,
        implement_cron_bug: bool,
    ) -> Result<Self, String> {
        let parsed = CronExpr::parse(expr, hash_id, second_at_beginning)?;
        Ok(CronSchedule {
            expr: parsed,
            day_or,
            implement_cron_bug,
        })
    }

    /// Check if this expression has a seconds field
    pub fn has_seconds(&self) -> bool {
        self.expr.has_seconds
    }

    /// Get the next matching datetime after the given datetime
    pub fn next_from(&self, dt: NaiveDateTime) -> Option<NaiveDateTime> {
        // Check if we need to use the day_or split logic
        // Python croniter: when both day and weekday are specified (not wildcards),
        // calculate separately and take the earlier result
        if self.should_split_day_dow() {
            let t1 = self.next_from_day_only(dt);
            let t2 = self.next_from_dow_only(dt);

            // Python croniter behavior: if EITHER t1 or t2 fails (raises exception),
            // the whole function fails. We match this by requiring BOTH to succeed.
            // This is because Python's _calc raises CroniterBadDateError on failure,
            // and there's no try-catch around the t1/t2 calculations.
            match (t1, t2) {
                (Some(d1), Some(d2)) => Some(if d1 < d2 { d1 } else { d2 }),
                _ => None, // If either fails, return None (matches Python's exception behavior)
            }
        } else {
            self.next_from_internal(dt)
        }
    }

    /// Get the previous matching datetime before the given datetime
    pub fn prev_from(&self, dt: NaiveDateTime) -> Option<NaiveDateTime> {
        // Check if we need to use the day_or split logic
        if self.should_split_day_dow() {
            let t1 = self.prev_from_day_only(dt);
            let t2 = self.prev_from_dow_only(dt);

            // Python croniter behavior: if EITHER t1 or t2 fails (raises exception),
            // the whole function fails. We match this by requiring BOTH to succeed.
            match (t1, t2) {
                (Some(d1), Some(d2)) => Some(if d1 > d2 { d1 } else { d2 }),
                _ => None, // If either fails, return None (matches Python's exception behavior)
            }
        } else {
            self.prev_from_internal(dt)
        }
    }

    /// Check if we should split day and dow calculation (Python croniter behavior)
    fn should_split_day_dow(&self) -> bool {
        if !self.day_or {
            return false;
        }

        // NOTE: Python croniter DOES use day_or split even when nth_weekday is present.
        // The nth_weekday_of_month is preserved during t1 calculation, which means
        // t1 = day AND nth_weekday, t2 = weekday only.
        // This is different from what we previously thought.

        // Python's condition: expanded[DAY_FIELD][0] != "*" and expanded[DOW_FIELD][0] != "*"
        // This means we check if the ORIGINAL expression was "*", not if it covers all values
        // So "0-6" for weekday is NOT the same as "*" in Python's logic
        if self.expr.day_is_star || self.expr.weekday_is_star {
            return false;
        }

        // Python's implement_cron_bug logic:
        // If implement_cron_bug=True AND (day_expr starts with '*' OR dow_expr starts with '*'),
        // then bypass OR logic and use AND instead.
        // But if neither expression starts with '*', use OR logic even with implement_cron_bug=True.
        if self.implement_cron_bug {
            if self.expr.day_starts_with_star || self.expr.weekday_starts_with_star {
                // Cron bug applies: bypass OR logic, use AND instead
                return false;
            }
            // Neither starts with *, so use OR logic even with implement_cron_bug=True
        }

        true
    }

    /// Calculate next match using only day-of-month (weekday as wildcard)
    fn next_from_day_only(&self, dt: NaiveDateTime) -> Option<NaiveDateTime> {
        // Create a temporary schedule with weekday as wildcard
        // BUT keep nth_weekdays - Python croniter keeps nth_weekday_of_month
        // even when calculating day-only, so nth constraints still apply
        let mut temp_expr = self.expr.clone();
        temp_expr.weekdays = (0..7).collect();
        // Keep nth_weekdays! Python's _calc still uses nth_weekday_of_month for t1

        let temp_schedule = CronSchedule {
            expr: temp_expr,
            day_or: self.day_or,
            implement_cron_bug: self.implement_cron_bug,
        };
        temp_schedule.next_from_internal(dt)
    }

    /// Calculate next match using only day-of-week (day as wildcard)
    fn next_from_dow_only(&self, dt: NaiveDateTime) -> Option<NaiveDateTime> {
        // Create a temporary schedule with day as wildcard
        let mut temp_expr = self.expr.clone();
        temp_expr.days = (1..32).collect();
        temp_expr.last_day = false;

        let temp_schedule = CronSchedule {
            expr: temp_expr,
            day_or: self.day_or,
            implement_cron_bug: self.implement_cron_bug,
        };
        temp_schedule.next_from_internal(dt)
    }

    /// Calculate prev match using only day-of-month (weekday as wildcard)
    fn prev_from_day_only(&self, dt: NaiveDateTime) -> Option<NaiveDateTime> {
        // Keep nth_weekdays - Python's _calc still uses nth_weekday_of_month for t1
        let mut temp_expr = self.expr.clone();
        temp_expr.weekdays = (0..7).collect();
        // Keep nth_weekdays!

        let temp_schedule = CronSchedule {
            expr: temp_expr,
            day_or: self.day_or,
            implement_cron_bug: self.implement_cron_bug,
        };
        temp_schedule.prev_from_internal(dt)
    }

    /// Calculate prev match using only day-of-week (day as wildcard)
    fn prev_from_dow_only(&self, dt: NaiveDateTime) -> Option<NaiveDateTime> {
        let mut temp_expr = self.expr.clone();
        temp_expr.days = (1..32).collect();
        temp_expr.last_day = false;

        let temp_schedule = CronSchedule {
            expr: temp_expr,
            day_or: self.day_or,
            implement_cron_bug: self.implement_cron_bug,
        };
        temp_schedule.prev_from_internal(dt)
    }

    /// Internal next_from implementation
    fn next_from_internal(&self, dt: NaiveDateTime) -> Option<NaiveDateTime> {
        // Start from the next second
        let mut current = dt + chrono::Duration::seconds(1);

        // Limit search to prevent infinite loops (50 years)
        let max_date = dt + chrono::Duration::days(50 * 365);

        while current < max_date {
            if let Some(next) = self.find_next_match(current) {
                return Some(next);
            }
            // Move to next month if no match found in current search
            current = self.advance_month(current)?;
        }

        None
    }

    /// Internal prev_from implementation
    fn prev_from_internal(&self, dt: NaiveDateTime) -> Option<NaiveDateTime> {
        // Python's _calc behavior:
        // - For 5-field expressions: offset is 60 seconds if now % 60 == 0, else 1 second
        // - For 6+ field expressions: offset is 1 second
        // But when called from match() with microseconds added, we need to handle that case

        // If the input has nanoseconds (from microsecond addition in match),
        // we should check the current second first
        let has_subsecond = dt.nanosecond() > 0;

        // Start from the appropriate time
        let mut current = if has_subsecond {
            // If there are subseconds, truncate to the current second
            // This allows finding a match at the current second
            dt.with_nanosecond(0).unwrap_or(dt)
        } else {
            // Otherwise, start from the previous second
            dt - chrono::Duration::seconds(1)
        };

        // Limit search to prevent infinite loops (50 years back)
        let min_date = dt - chrono::Duration::days(50 * 365);

        while current > min_date {
            if let Some(prev) = self.find_prev_match(current) {
                return Some(prev);
            }
            // Move to previous month if no match found
            current = self.retreat_month(current)?;
        }

        None
    }

    /// Check if a datetime matches the schedule
    pub fn matches(&self, dt: NaiveDateTime) -> bool {
        // Check year (if years field is specified)
        if self.expr.has_years && !self.expr.years.contains(&(dt.year() as u32)) {
            return false;
        }

        // Check seconds
        if !self.expr.seconds.contains(&(dt.second() as u32)) {
            return false;
        }

        // Check minutes
        if !self.expr.minutes.contains(&(dt.minute() as u32)) {
            return false;
        }

        // Check hours
        if !self.expr.hours.contains(&(dt.hour() as u32)) {
            return false;
        }

        // Check month
        if !self.expr.months.contains(&(dt.month() as u32)) {
            return false;
        }

        // Check day matching (day of month and/or day of week)
        self.matches_day(dt)
    }

    /// Get expanded field values for Python interface
    pub fn get_expanded(&self) -> Vec<Vec<i32>> {
        let mut result = vec![
            self.expr.minutes.iter().map(|&v| v as i32).collect(),
            self.expr.hours.iter().map(|&v| v as i32).collect(),
            self.expr.days.iter().map(|&v| v as i32).collect(),
            self.expr.months.iter().map(|&v| v as i32).collect(),
            self.expr.weekdays.iter().map(|&v| v as i32).collect(),
        ];

        // Add seconds field only if expression has seconds (6+ fields)
        if self.expr.has_seconds {
            result.push(self.expr.seconds.iter().map(|&v| v as i32).collect());
        }

        // Add years field if present (7 fields)
        if self.expr.has_years {
            result.push(self.expr.years.iter().map(|&v| v as i32).collect());
        }

        result
    }

    /// Check if the day (day-of-month and day-of-week) matches
    fn matches_day(&self, dt: NaiveDateTime) -> bool {
        let dom_matches = self.matches_day_of_month(dt);
        let dow_matches = self.matches_day_of_week(dt);

        // Check if day or weekday fields are wildcards (all values)
        let day_is_wildcard = self.expr.days.len() == 31 &&
            self.expr.days.iter().enumerate().all(|(i, &v)| v == (i + 1) as u32) &&
            !self.expr.last_day;
        let weekday_is_wildcard = self.expr.weekdays.len() == 7 &&
            self.expr.weekdays.iter().enumerate().all(|(i, &v)| v == i as u32) &&
            self.expr.nth_weekdays.is_empty();

        // NOTE: Python croniter uses OR logic for day/weekday even with #N syntax
        // when day_or=true. The #N constraint is checked within matches_day_of_week.
        // The day_or split logic in get_next/get_prev handles the complex interaction
        // between day and nth_weekday.

        if self.implement_cron_bug {
            // Cron bug: use AND logic
            dom_matches && dow_matches
        } else if self.day_or {
            // Standard behavior: OR logic when both are specified
            // But if one is wildcard, only check the other
            if day_is_wildcard && !weekday_is_wildcard {
                dow_matches
            } else if weekday_is_wildcard && !day_is_wildcard {
                dom_matches
            } else if day_is_wildcard && weekday_is_wildcard {
                true
            } else {
                // Both are specified (not wildcards), use OR
                dom_matches || dow_matches
            }
        } else {
            // day_or=false: AND logic
            dom_matches && dow_matches
        }
    }

    /// Check if day of month matches
    fn matches_day_of_month(&self, dt: NaiveDateTime) -> bool {
        let day = dt.day() as u32;

        // Check for last day of month
        if self.expr.last_day {
            let last_day = last_day_of_month(dt.year(), dt.month());
            if day == last_day {
                return true;
            }
        }

        // Check regular day values
        self.expr.days.contains(&day)
    }

    /// Check if day of week matches
    fn matches_day_of_week(&self, dt: NaiveDateTime) -> bool {
        let weekday = chrono_weekday_to_cron(dt.weekday());

        // Check nth weekday constraints
        if !self.expr.nth_weekdays.is_empty() {
            let day = dt.day();
            let nth = ((day - 1) / 7 + 1) as u32;

            for &(wd, n) in &self.expr.nth_weekdays {
                if wd == weekday {
                    if n == 0 {
                        // n == 0 means last occurrence (lN syntax)
                        // Check if this is the last occurrence of this weekday in the month
                        if is_last_weekday_of_month(dt, weekday) {
                            return true;
                        }
                    } else if n == nth {
                        return true;
                    }
                }
            }

            // nth_weekdays are specified but none match
            // This is a mandatory constraint, so return false
            return false;
        }

        self.expr.weekdays.contains(&weekday)
    }

    /// Find the next matching datetime starting from current
    fn find_next_match(&self, start: NaiveDateTime) -> Option<NaiveDateTime> {
        let mut year = start.year();
        let mut month = start.month();
        let mut day = start.day();
        let mut hour = start.hour();
        let mut minute = start.minute();
        let mut second = start.second();

        // Find next matching year (if years field is specified)
        if self.expr.has_years {
            let orig_year = year;
            year = self.find_next_in_list(&self.expr.years, year as u32)? as i32;
            if year > orig_year {
                month = 1;
                day = 1;
                hour = 0;
                minute = 0;
                second = 0;
            }
        }

        // Find next matching month
        let orig_month = month;
        month = self.find_next_in_list(&self.expr.months, month as u32)? as u32;

        if month < orig_month {
            year += 1;
            // Check if new year is valid
            if self.expr.has_years && !self.expr.years.contains(&(year as u32)) {
                year = self.find_next_in_list(&self.expr.years, year as u32)? as i32;
            }
            day = 1;
            hour = 0;
            minute = 0;
            second = 0;
        } else if month > orig_month {
            day = 1;
            hour = 0;
            minute = 0;
            second = 0;
        }

        // Check if we have nth_weekday constraints
        let has_nth_weekday = !self.expr.nth_weekdays.is_empty();

        // Find next matching day
        let max_iterations = 366 * 2; // Prevent infinite loops
        for _ in 0..max_iterations {
            // Validate date - handle day overflow like Python's relativedelta
            let last_day = last_day_of_month(year, month);

            if day > last_day {
                // Carry over excess days to next month (like relativedelta)
                let overflow = day - last_day;
                let orig_month = month;
                month += 1;
                if month > 12 {
                    month = 1;
                    year += 1;
                }
                // Find next valid month
                let next_month = self.find_next_in_list(&self.expr.months, month as u32)? as u32;
                if next_month < month {
                    year += 1;
                    if self.expr.has_years && !self.expr.years.contains(&(year as u32)) {
                        year = self.find_next_in_list(&self.expr.years, year as u32)? as i32;
                    }
                }

                // When overflowing to next month, always start from day 1
                // This ensures we don't skip valid days at the beginning of the month
                // (e.g., if day=31 overflows by 2 days, we should check day 1, not day 2)
                day = 1;
                month = next_month;
                hour = 0;
                minute = 0;
                second = 0;
                continue;
            }

            let date = NaiveDate::from_ymd_opt(year, month, day)?;
            let time = NaiveTime::from_hms_opt(hour, minute, second)?;
            let dt = NaiveDateTime::new(date, time);

            // When nth_weekday is specified, find the next valid day directly
            // Must also check day-of-month constraint (Python behavior: AND logic)
            if has_nth_weekday {
                let nth_days = get_nth_weekday_days(year, month, &self.expr.nth_weekdays);

                // Check if day field is effectively a wildcard
                let day_is_wildcard = self.expr.days.len() == 31 &&
                    self.expr.days.iter().enumerate().all(|(i, &v)| v == (i + 1) as u32) &&
                    !self.expr.last_day;

                // Find valid days that match BOTH day constraint AND nth_weekday constraint
                let valid_days: Vec<u32> = if day_is_wildcard {
                    // Day is wildcard, only check nth_weekday
                    nth_days
                } else {
                    // Must match both day constraint and nth_weekday
                    let last_day_val = last_day_of_month(year, month);
                    nth_days.into_iter().filter(|&nth_day| {
                        // Check if nth_day matches day constraint
                        if self.expr.last_day && nth_day == last_day_val {
                            return true;
                        }
                        self.expr.days.contains(&nth_day)
                    }).collect()
                };

                // Find the first valid_day >= current day
                let next_valid_day = valid_days.iter().find(|&&d| d >= day);

                match next_valid_day {
                    Some(&valid_day) => {
                        if valid_day > day {
                            // Jump to the valid_day
                            day = valid_day;
                            hour = 0;
                            minute = 0;
                            second = 0;
                            continue;
                        }
                        // day == valid_day, check time
                    }
                    None => {
                        // No valid day in this month, move to next month
                        day = 1;
                        hour = 0;
                        minute = 0;
                        second = 0;
                        month += 1;
                        if month > 12 {
                            month = 1;
                            year += 1;
                        }
                        month = self.find_next_in_list(&self.expr.months, month as u32)? as u32;
                        continue;
                    }
                }
            } else {
                // Regular day matching - use Python-style diff calculation
                let diff = self.get_next_day_diff(day, month, year);
                if let Some(d) = diff {
                    if d != 0 {
                        // Add diff days (may overflow to next month)
                        day += d;
                        hour = 0;
                        minute = 0;
                        second = 0;
                        continue;
                    }
                    // diff == 0 means current day matches, check time
                } else {
                    // No matching day found, this shouldn't happen with valid expressions
                    day += 1;
                    hour = 0;
                    minute = 0;
                    second = 0;
                    continue;
                }
            }

            // Find next matching hour
            let orig_hour = hour;
            if let Some(next_hour) = self.find_next_in_list(&self.expr.hours, hour as u32) {
                hour = next_hour as u32;
                if hour < orig_hour {
                    // Wrapped, move to next day
                    day += 1;
                    hour = 0;
                    minute = 0;
                    second = 0;
                    continue;
                } else if hour > orig_hour {
                    minute = 0;
                    second = 0;
                }
            } else {
                day += 1;
                hour = 0;
                minute = 0;
                second = 0;
                continue;
            }

            // Find next matching minute
            let orig_minute = minute;
            if let Some(next_minute) = self.find_next_in_list(&self.expr.minutes, minute as u32) {
                minute = next_minute as u32;
                if minute < orig_minute {
                    // Wrapped, move to next hour
                    hour += 1;
                    minute = 0;
                    second = 0;
                    if hour > 23 {
                        day += 1;
                        hour = 0;
                    }
                    continue;
                } else if minute > orig_minute {
                    second = 0;
                }
            } else {
                hour += 1;
                minute = 0;
                second = 0;
                if hour > 23 {
                    day += 1;
                    hour = 0;
                }
                continue;
            }

            // Find next matching second
            if let Some(next_second) = self.find_next_in_list(&self.expr.seconds, second as u32) {
                second = next_second as u32;
                if second < start.second() && minute == start.minute() && hour == start.hour()
                    && day == start.day() && month == start.month() as u32 && year == start.year() {
                    // Wrapped on same minute, move to next minute
                    minute += 1;
                    second = 0;
                    if minute > 59 {
                        hour += 1;
                        minute = 0;
                        if hour > 23 {
                            day += 1;
                            hour = 0;
                        }
                    }
                    continue;
                }
            } else {
                minute += 1;
                second = 0;
                if minute > 59 {
                    hour += 1;
                    minute = 0;
                    if hour > 23 {
                        day += 1;
                        hour = 0;
                    }
                }
                continue;
            }

            // Construct and validate final datetime
            let final_date = NaiveDate::from_ymd_opt(year, month, day)?;
            let final_time = NaiveTime::from_hms_opt(hour, minute, second)?;
            let result = NaiveDateTime::new(final_date, final_time);

            // Final validation
            if self.matches(result) && result > start - chrono::Duration::seconds(1) {
                return Some(result);
            }

            // Move to next day
            day += 1;
            hour = 0;
            minute = 0;
            second = 0;
        }

        None
    }

    /// Find the previous matching datetime ending at current
    fn find_prev_match(&self, start: NaiveDateTime) -> Option<NaiveDateTime> {
        let mut year = start.year();
        let mut month = start.month();
        let mut day = start.day();
        let mut hour = start.hour();
        let mut minute = start.minute();
        let mut second = start.second();

        // Find previous matching month
        let orig_month = month;
        month = self.find_prev_in_list(&self.expr.months, month as u32)? as u32;

        if month > orig_month {
            year -= 1;
            day = last_day_of_month(year, month);
            hour = 23;
            minute = 59;
            second = 59;
        } else if month < orig_month {
            day = last_day_of_month(year, month);
            hour = 23;
            minute = 59;
            second = 59;
        }

        // Check if we have nth_weekday constraints
        let has_nth_weekday = !self.expr.nth_weekdays.is_empty();

        let max_iterations = 366 * 2;
        for _ in 0..max_iterations {
            // Validate date
            let last_day = last_day_of_month(year, month);
            if day > last_day {
                day = last_day;
            }

            if day < 1 {
                // Move to previous month
                let mut new_month = month as i32 - 1;
                let mut new_year = year;
                if new_month < 1 {
                    new_month = 12;
                    new_year -= 1;
                }

                // Find the previous valid month in the expression
                let prev_month = self.find_prev_in_list(&self.expr.months, new_month as u32)? as u32;

                // If prev_month > new_month, we wrapped, so decrease year
                if prev_month > new_month as u32 {
                    new_year -= 1;
                }

                year = new_year;
                month = prev_month;
                day = last_day_of_month(year, month);
                hour = 23;
                minute = 59;
                second = 59;
                continue;
            }

            let date = NaiveDate::from_ymd_opt(year, month, day)?;
            let time = NaiveTime::from_hms_opt(hour, minute, second)?;
            let dt = NaiveDateTime::new(date, time);

            // When nth_weekday is specified, find the previous valid day directly
            // Must also check day-of-month constraint (Python behavior: AND logic)
            if has_nth_weekday {
                let nth_days = get_nth_weekday_days(year, month, &self.expr.nth_weekdays);

                // Check if day field is effectively a wildcard
                let day_is_wildcard = self.expr.days.len() == 31 &&
                    self.expr.days.iter().enumerate().all(|(i, &v)| v == (i + 1) as u32) &&
                    !self.expr.last_day;

                // Find valid days that match BOTH day constraint AND nth_weekday constraint
                let valid_days: Vec<u32> = if day_is_wildcard {
                    // Day is wildcard, only check nth_weekday
                    nth_days
                } else {
                    // Must match both day constraint and nth_weekday
                    let last_day_val = last_day_of_month(year, month);
                    nth_days.into_iter().filter(|&nth_day| {
                        // Check if nth_day matches day constraint
                        if self.expr.last_day && nth_day == last_day_val {
                            return true;
                        }
                        self.expr.days.contains(&nth_day)
                    }).collect()
                };

                // Find the last valid_day <= current day
                let prev_valid_day = valid_days.iter().rev().find(|&&d| d <= day);

                match prev_valid_day {
                    Some(&valid_day) => {
                        if valid_day < day {
                            // Jump to the valid_day
                            day = valid_day;
                            hour = 23;
                            minute = 59;
                            second = 59;
                            continue;
                        }
                        // day == valid_day, check time
                    }
                    None => {
                        // No valid day in this month, move to previous month
                        month -= 1;
                        if month < 1 {
                            month = 12;
                            year -= 1;
                        }
                        month = self.find_prev_in_list(&self.expr.months, month as u32)? as u32;
                        day = last_day_of_month(year, month);
                        hour = 23;
                        minute = 59;
                        second = 59;
                        continue;
                    }
                }
            } else {
                // Regular day matching
                // Check if weekday is effectively a wildcard
                let weekday_is_wildcard = self.expr.weekday_is_star ||
                    (self.expr.weekdays.len() == 7 && self.expr.nth_weekdays.is_empty());

                // First check if day_of_month matches
                if !self.matches_day_of_month(dt) {
                    // Day doesn't match, use Python-style diff calculation
                    let diff = self.get_prev_day_diff(day, month, year);
                    if let Some(d) = diff {
                        if d != 0 {
                            // Add diff days (negative, may underflow to previous month)
                            let new_day = day as i32 + d;
                            if new_day < 1 {
                                // Underflow to previous month
                                let mut temp_month = month as i32 - 1;
                                let mut temp_year = year;
                                if temp_month < 1 {
                                    temp_month = 12;
                                    temp_year -= 1;
                                }

                                // Check if the new month is valid
                                if self.expr.months.contains(&(temp_month as u32)) {
                                    // Month is valid, start from its last day
                                    // This ensures we don't skip valid days like Feb 29
                                    year = temp_year;
                                    month = temp_month as u32;
                                    day = last_day_of_month(year, month);
                                } else {
                                    // Month is not valid, find previous valid month
                                    // and start from its last day (Python behavior)
                                    let prev_month = self.find_prev_in_list(&self.expr.months, temp_month as u32)? as u32;

                                    // If prev_month > temp_month, we wrapped, so decrease year
                                    if prev_month > temp_month as u32 {
                                        temp_year -= 1;
                                    }

                                    year = temp_year;
                                    month = prev_month;
                                    day = last_day_of_month(year, month);
                                }
                            } else {
                                day = new_day as u32;
                            }
                            hour = 23;
                            minute = 59;
                            second = 59;
                            continue;
                        }
                    }
                    // diff == 0 or None, move to previous day
                    day -= 1;
                    hour = 23;
                    minute = 59;
                    second = 59;
                    continue;
                }

                // Day of month matches, now check weekday if day_or=false
                if !self.day_or && !weekday_is_wildcard {
                    // Need weekday to match too
                    if !self.matches_day_of_week(dt) {
                        // Weekday doesn't match, move to previous day
                        day -= 1;
                        hour = 23;
                        minute = 59;
                        second = 59;
                        continue;
                    }
                }
                // Day matches, continue to check time
            }

            // Find previous matching hour
            let orig_hour = hour;
            if let Some(prev_hour) = self.find_prev_in_list(&self.expr.hours, hour as u32) {
                hour = prev_hour as u32;
                if hour > orig_hour {
                    // Wrapped, move to previous day
                    day -= 1;
                    hour = 23;
                    minute = 59;
                    second = 59;
                    continue;
                } else if hour < orig_hour {
                    minute = 59;
                    second = 59;
                }
            } else {
                day -= 1;
                hour = 23;
                minute = 59;
                second = 59;
                continue;
            }

            // Find previous matching minute
            let orig_minute = minute;
            if let Some(prev_minute) = self.find_prev_in_list(&self.expr.minutes, minute as u32) {
                minute = prev_minute as u32;
                if minute > orig_minute {
                    // Wrapped, move to previous hour
                    hour = hour.saturating_sub(1);
                    if hour == 0 && orig_hour == 0 {
                        day -= 1;
                        hour = 23;
                    }
                    minute = 59;
                    second = 59;
                    continue;
                } else if minute < orig_minute {
                    second = 59;
                }
            } else {
                if hour > 0 {
                    hour -= 1;
                } else {
                    day -= 1;
                    hour = 23;
                }
                minute = 59;
                second = 59;
                continue;
            }

            // Find previous matching second
            if let Some(prev_second) = self.find_prev_in_list(&self.expr.seconds, second as u32) {
                second = prev_second as u32;
                if second > start.second() && minute == start.minute() && hour == start.hour()
                    && day == start.day() && month == start.month() as u32 && year == start.year() {
                    // Wrapped on same minute, move to previous minute
                    if minute > 0 {
                        minute -= 1;
                    } else {
                        if hour > 0 {
                            hour -= 1;
                        } else {
                            day -= 1;
                            hour = 23;
                        }
                        minute = 59;
                    }
                    second = 59;
                    continue;
                }
            } else {
                if minute > 0 {
                    minute -= 1;
                } else {
                    if hour > 0 {
                        hour -= 1;
                    } else {
                        day -= 1;
                        hour = 23;
                    }
                    minute = 59;
                }
                second = 59;
                continue;
            }

            // Construct and validate final datetime
            let final_date = NaiveDate::from_ymd_opt(year, month, day)?;
            let final_time = NaiveTime::from_hms_opt(hour, minute, second)?;
            let result = NaiveDateTime::new(final_date, final_time);

            // Final validation
            if self.matches(result) && result < start + chrono::Duration::seconds(1) {
                return Some(result);
            }

            // Move to previous day
            day -= 1;
            hour = 23;
            minute = 59;
            second = 59;
        }

        None
    }

    /// Calculate the diff to the next matching day (Python's _get_next_nearest_diff logic)
    /// Returns the number of days to add to reach the next matching day
    fn get_next_day_diff(&self, current_day: u32, month: u32, year: i32) -> Option<u32> {
        let last_day = last_day_of_month(year, month);

        // Check for last day of month first
        if self.expr.last_day && current_day == last_day {
            return Some(0);
        }

        // Build the list of valid days for this month
        let mut days_to_check: Vec<u32> = self.expr.days.iter()
            .filter(|&&d| d <= 31)  // Filter out invalid days
            .copied()
            .collect();

        // Add last day if specified (as 'l' marker, we use last_day value)
        if self.expr.last_day {
            days_to_check.push(last_day);
            days_to_check.sort();
            days_to_check.dedup();
        }

        if days_to_check.is_empty() {
            return None;
        }

        // Python's _get_next_nearest_diff logic:
        // Find first value >= current_day
        // Note: Python does NOT clamp values to last_day here - it compares directly
        // This allows values like 31 to be found even in months with fewer days
        for &d in &days_to_check {
            if d >= current_day {
                return Some(d - current_day);
            }
        }

        // No value >= current_day found, wrap to next cycle
        // diff = days_to_check[0] - current_day + range_val (last_day)
        let first_day = days_to_check[0];
        Some(first_day + last_day - current_day)
    }

    /// Calculate the diff to the previous matching day (Python's _get_prev_nearest_diff logic)
    /// Returns a negative number of days to subtract to reach the previous matching day
    fn get_prev_day_diff(&self, current_day: u32, month: u32, year: i32) -> Option<i32> {
        // Python uses a static DAYS array that doesn't account for leap years
        // DAYS = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
        const DAYS: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

        // Python: days_in_prev_month = DAYS[(month - 2) % MONTHS_IN_YEAR]
        let days_in_prev_month = DAYS[((month as i32 - 2).rem_euclid(12)) as usize];
        let last_day = last_day_of_month(year, month);

        // Check for last day of month first (Python: if "l" in expanded[DAY_FIELD] and days == d.day)
        if self.expr.last_day && current_day == last_day {
            return Some(0);
        }

        // If only 'l' is specified (no other days), use special logic
        // Python: if "l" in candidates: return -x
        if self.expr.last_day && self.expr.days.is_empty() {
            // Only 'l' is specified, return -current_day to go to previous month's last day
            return Some(-(current_day as i32));
        }

        // Build the list of valid days
        let mut days_to_check: Vec<u32> = self.expr.days.iter()
            .filter(|&&d| d <= 31)
            .copied()
            .collect();

        // Add last day if specified
        if self.expr.last_day {
            days_to_check.push(last_day);
            days_to_check.sort();
            days_to_check.dedup();
        }

        if days_to_check.is_empty() {
            return None;
        }

        // Python's _get_prev_nearest_diff logic:
        // candidates = to_check[:]; candidates.reverse()
        // for d in candidates:
        //     if d != "l" and d <= x:
        //         return d - x
        // if "l" in candidates:
        //     return -x
        // ... wrap logic using days_in_prev_month

        let mut candidates = days_to_check.clone();
        candidates.reverse();

        // Find last value <= current_day
        for &d in &candidates {
            if d <= current_day {
                return Some(d as i32 - current_day as i32);
            }
        }

        // If 'l' (last_day) is specified but we didn't match above, return -current_day
        if self.expr.last_day {
            return Some(-(current_day as i32));
        }

        // No value <= current_day found, wrap to previous cycle
        // Python logic:
        // candidate = candidates[0]  (largest value after reverse)
        // for c in candidates:
        //     if c <= range_val:
        //         candidate = c
        //         break
        // if candidate > range_val:
        //     return -range_val
        // return candidate - x - range_val

        let mut candidate = candidates[0];
        for &c in &candidates {
            if c <= days_in_prev_month {
                candidate = c;
                break;
            }
        }

        if candidate > days_in_prev_month {
            return Some(-(days_in_prev_month as i32));
        }

        Some(candidate as i32 - current_day as i32 - days_in_prev_month as i32)
    }

    /// Find the next value in a sorted list >= target, wrapping if needed
    fn find_next_in_list(&self, list: &[u32], target: u32) -> Option<u32> {
        if list.is_empty() {
            return None;
        }

        // Find first value >= target
        for &val in list {
            if val >= target {
                return Some(val);
            }
        }

        // Wrap to beginning
        Some(list[0])
    }

    /// Find the previous value in a sorted list <= target, wrapping if needed
    fn find_prev_in_list(&self, list: &[u32], target: u32) -> Option<u32> {
        if list.is_empty() {
            return None;
        }

        // Find last value <= target
        let mut result = None;
        for &val in list {
            if val <= target {
                result = Some(val);
            } else {
                break;
            }
        }

        // If found, return it; otherwise wrap to end
        result.or_else(|| list.last().copied())
    }

    /// Advance to the start of the next month
    fn advance_month(&self, dt: NaiveDateTime) -> Option<NaiveDateTime> {
        let (year, month) = if dt.month() == 12 {
            (dt.year() + 1, 1)
        } else {
            (dt.year(), dt.month() + 1)
        };

        let date = NaiveDate::from_ymd_opt(year, month, 1)?;
        let time = NaiveTime::from_hms_opt(0, 0, 0)?;
        Some(NaiveDateTime::new(date, time))
    }

    /// Retreat to the end of the previous month
    fn retreat_month(&self, dt: NaiveDateTime) -> Option<NaiveDateTime> {
        let (year, month) = if dt.month() == 1 {
            (dt.year() - 1, 12)
        } else {
            (dt.year(), dt.month() - 1)
        };

        let last_day = last_day_of_month(year, month);
        let date = NaiveDate::from_ymd_opt(year, month, last_day)?;
        let time = NaiveTime::from_hms_opt(23, 59, 59)?;
        Some(NaiveDateTime::new(date, time))
    }
}

/// Convert chrono Weekday to cron weekday (0=Sunday, 6=Saturday)
fn chrono_weekday_to_cron(weekday: Weekday) -> u32 {
    match weekday {
        Weekday::Sun => 0,
        Weekday::Mon => 1,
        Weekday::Tue => 2,
        Weekday::Wed => 3,
        Weekday::Thu => 4,
        Weekday::Fri => 5,
        Weekday::Sat => 6,
    }
}

/// Get the last day of a month, handling leap years
fn last_day_of_month(year: i32, month: u32) -> u32 {
    match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 => {
            if is_leap_year(year) {
                29
            } else {
                28
            }
        }
        _ => 31, // Fallback
    }
}

/// Check if a year is a leap year
fn is_leap_year(year: i32) -> bool {
    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}

/// Check if a given date is the last occurrence of its weekday in the month
fn is_last_weekday_of_month(dt: NaiveDateTime, weekday: u32) -> bool {
    let current_day = dt.day();
    let last_day = last_day_of_month(dt.year(), dt.month());

    // Check if adding 7 days would go past the end of the month
    // If so, this is the last occurrence of this weekday
    current_day + 7 > last_day
}

/// Get all days in a month that match the nth weekday constraints
/// Returns a sorted vector of day numbers (1-31)
fn get_nth_weekday_days(year: i32, month: u32, nth_weekdays: &[(u32, u32)]) -> Vec<u32> {
    let mut result = Vec::new();
    let last_day = last_day_of_month(year, month);

    // Find the first day of the month and its weekday
    let first_date = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
    let first_weekday = chrono_weekday_to_cron(first_date.weekday());

    for &(target_weekday, nth) in nth_weekdays {
        // Calculate the first occurrence of target_weekday in this month
        let days_until_target = (target_weekday as i32 - first_weekday as i32 + 7) % 7;
        let first_occurrence = 1 + days_until_target as u32;

        if nth == 0 {
            // Last occurrence of this weekday
            // Find all occurrences and take the last one
            let mut day = first_occurrence;
            let mut last_valid = 0;
            while day <= last_day {
                last_valid = day;
                day += 7;
            }
            if last_valid > 0 {
                result.push(last_valid);
            }
        } else {
            // Nth occurrence (1-5)
            let day = first_occurrence + (nth - 1) * 7;
            if day <= last_day {
                result.push(day);
            }
        }
    }

    result.sort();
    result.dedup();
    result
}

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

    // 生成合法的 cron 分钟值 (0-59)
    fn arb_minute() -> impl Strategy<Value = u32> {
        0u32..60
    }

    // 生成合法的 cron 小时值 (0-23)
    fn arb_hour() -> impl Strategy<Value = u32> {
        0u32..24
    }

    // 生成合法的 cron 日值 (1-31)
    fn arb_day() -> impl Strategy<Value = u32> {
        1u32..32
    }

    // 生成合法的 cron 月值 (1-12)
    fn arb_month() -> impl Strategy<Value = u32> {
        1u32..13
    }

    // 生成合法的 cron 周值 (0-6)
    fn arb_weekday() -> impl Strategy<Value = u32> {
        0u32..7
    }

    proptest! {
        #![proptest_config(ProptestConfig::with_cases(10000))]

        #[test]
        fn prop_next_always_greater(minute in arb_minute(), hour in arb_hour()) {
            let expr = format!("{} {} * * *", minute, hour);
            let schedule = CronSchedule::parse(&expr, None, false, true, false).unwrap();
            let start = NaiveDateTime::parse_from_str("2024-01-15 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
            if let Some(next) = schedule.next_from(start) {
                prop_assert!(next > start);
            }
        }

        #[test]
        fn prop_prev_always_less(minute in arb_minute(), hour in arb_hour()) {
            let expr = format!("{} {} * * *", minute, hour);
            let schedule = CronSchedule::parse(&expr, None, false, true, false).unwrap();
            let start = NaiveDateTime::parse_from_str("2024-01-15 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
            if let Some(prev) = schedule.prev_from(start) {
                prop_assert!(prev < start);
            }
        }

        #[test]
        fn prop_next_matches_schedule(minute in arb_minute(), hour in arb_hour()) {
            let expr = format!("{} {} * * *", minute, hour);
            let schedule = CronSchedule::parse(&expr, None, false, true, false).unwrap();
            let start = NaiveDateTime::parse_from_str("2024-01-15 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
            if let Some(next) = schedule.next_from(start) {
                prop_assert!(schedule.matches(next));
            }
        }

        #[test]
        fn prop_consecutive_next_increasing(minute in arb_minute()) {
            let expr = format!("{} * * * *", minute);
            let schedule = CronSchedule::parse(&expr, None, false, true, false).unwrap();
            let start = NaiveDateTime::parse_from_str("2024-01-15 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();

            let mut current = start;
            for _ in 0..5 {
                if let Some(next) = schedule.next_from(current) {
                    prop_assert!(next > current);
                    current = next;
                } else {
                    break;
                }
            }
        }
    }

    #[test]
    fn test_next_simple() {
        let schedule = CronSchedule::parse("0 * * * *", None, false, true, false).unwrap();
        let dt = NaiveDateTime::parse_from_str("2024-01-15 10:30:00", "%Y-%m-%d %H:%M:%S").unwrap();
        let next = schedule.next_from(dt).unwrap();
        assert_eq!(next.hour(), 11);
        assert_eq!(next.minute(), 0);
    }

    #[test]
    fn test_prev_simple() {
        let schedule = CronSchedule::parse("0 * * * *", None, false, true, false).unwrap();
        let dt = NaiveDateTime::parse_from_str("2024-01-15 10:30:00", "%Y-%m-%d %H:%M:%S").unwrap();
        let prev = schedule.prev_from(dt).unwrap();
        assert_eq!(prev.hour(), 10);
        assert_eq!(prev.minute(), 0);
    }

    #[test]
    fn test_matches() {
        let schedule = CronSchedule::parse("30 10 * * *", None, false, true, false).unwrap();
        let dt = NaiveDateTime::parse_from_str("2024-01-15 10:30:00", "%Y-%m-%d %H:%M:%S").unwrap();
        assert!(schedule.matches(dt));

        let dt2 = NaiveDateTime::parse_from_str("2024-01-15 10:31:00", "%Y-%m-%d %H:%M:%S").unwrap();
        assert!(!schedule.matches(dt2));
    }

    #[test]
    fn test_last_day_of_month() {
        assert_eq!(last_day_of_month(2024, 2), 29); // Leap year
        assert_eq!(last_day_of_month(2023, 2), 28); // Non-leap year
        assert_eq!(last_day_of_month(2024, 1), 31);
        assert_eq!(last_day_of_month(2024, 4), 30);
    }

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

    #[test]
    fn test_weekday_conversion() {
        assert_eq!(chrono_weekday_to_cron(Weekday::Sun), 0);
        assert_eq!(chrono_weekday_to_cron(Weekday::Sat), 6);
    }

    #[test]
    fn test_day_or_logic() {
        // day_or=true: OR logic
        let schedule = CronSchedule::parse("0 0 15 * 1", None, false, true, false).unwrap();
        // Should match either day 15 OR Monday

        // day_or=false: AND logic
        let schedule = CronSchedule::parse("0 0 15 * 1", None, false, false, false).unwrap();
        // Should match day 15 AND Monday
    }

    #[test]
    fn test_month_boundary() {
        let schedule = CronSchedule::parse("59 23 31 * *", None, false, true, false).unwrap();
        let dt = NaiveDateTime::parse_from_str("2024-01-31 23:59:00", "%Y-%m-%d %H:%M:%S").unwrap();
        let next = schedule.next_from(dt).unwrap();
        // Should go to next month with 31 days
    }

    #[test]
    fn test_leap_year_feb29() {
        let schedule = CronSchedule::parse("0 0 29 2 *", None, false, true, false).unwrap();
        let dt = NaiveDateTime::parse_from_str("2024-02-28 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
        let next = schedule.next_from(dt).unwrap();
        assert_eq!(next.day(), 29);
        assert_eq!(next.month(), 2);
    }

    #[test]
    fn test_year_boundary() {
        let schedule = CronSchedule::parse("0 0 1 1 *", None, false, true, false).unwrap();
        let dt = NaiveDateTime::parse_from_str("2024-12-31 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
        let next = schedule.next_from(dt).unwrap();
        assert_eq!(next.year(), 2025);
    }
}
