use pyo3::prelude::*;
use pyo3::types::{PyDateTime, PyType};
use pyo3::exceptions::{PyValueError, PyTypeError};
use chrono::{DateTime, Datelike, Duration, NaiveDateTime, Timelike, Utc, Weekday, Local};

pub mod parser;
pub mod schedule;

use schedule::CronSchedule;

// Field index constants
const MINUTE_FIELD: i32 = 0;
const HOUR_FIELD: i32 = 1;
const DAY_FIELD: i32 = 2;
const MONTH_FIELD: i32 = 3;
const DOW_FIELD: i32 = 4;
const SECOND_FIELD: i32 = 5;
const YEAR_FIELD: i32 = 6;

// Custom exception types
pyo3::create_exception!(croniter_rs, CroniterError, PyValueError);
pyo3::create_exception!(croniter_rs, CroniterBadCronError, CroniterError);
pyo3::create_exception!(croniter_rs, CroniterBadDateError, CroniterError);
pyo3::create_exception!(croniter_rs, CroniterNotAlphaError, CroniterBadCronError);
pyo3::create_exception!(croniter_rs, CroniterUnsupportedSyntaxError, CroniterBadCronError);
pyo3::create_exception!(croniter_rs, CroniterBadTypeRangeError, PyTypeError);

/// Convert Python datetime to chrono NaiveDateTime
fn py_datetime_to_naive(py: Python<'_>, dt: &Bound<'_, PyAny>) -> PyResult<NaiveDateTime> {
    // Handle None - use current time
    if dt.is_none() {
        let now = Local::now().naive_local();
        return Ok(now);
    }

    // Try to extract datetime components
    let year: i32 = dt.getattr("year")?.extract()?;
    let month: u32 = dt.getattr("month")?.extract()?;
    let day: u32 = dt.getattr("day")?.extract()?;
    let hour: u32 = dt.getattr("hour").map(|h| h.extract().unwrap_or(0)).unwrap_or(0);
    let minute: u32 = dt.getattr("minute").map(|m| m.extract().unwrap_or(0)).unwrap_or(0);
    let second: u32 = dt.getattr("second").map(|s| s.extract().unwrap_or(0)).unwrap_or(0);
    let microsecond: u32 = dt.getattr("microsecond").map(|m| m.extract().unwrap_or(0)).unwrap_or(0);

    Ok(NaiveDateTime::new(
        chrono::NaiveDate::from_ymd_opt(year, month, day)
            .ok_or_else(|| PyValueError::new_err("Invalid date"))?,
        chrono::NaiveTime::from_hms_micro_opt(hour, minute, second, microsecond)
            .ok_or_else(|| PyValueError::new_err("Invalid time"))?,
    ))
}

/// Convert chrono NaiveDateTime to Python datetime
fn naive_to_py_datetime<'py>(py: Python<'py>, dt: NaiveDateTime) -> PyResult<Bound<'py, PyDateTime>> {
    PyDateTime::new(
        py,
        dt.year(),
        dt.month() as u8,
        dt.day() as u8,
        dt.hour() as u8,
        dt.minute() as u8,
        dt.second() as u8,
        dt.and_utc().timestamp_subsec_micros(),
        None,
    )
}

/// Convert NaiveDateTime to timestamp (float)
/// Treats the NaiveDateTime as UTC time (matching Python croniter's behavior)
/// Note: Python croniter's get_next(float) returns UTC timestamps
fn naive_to_timestamp(dt: NaiveDateTime) -> f64 {
    dt.and_utc().timestamp() as f64 + (dt.and_utc().timestamp_subsec_micros() as f64 / 1_000_000.0)
}

/// Convert timestamp to NaiveDateTime
/// Treats the timestamp as UTC (matching Python croniter's behavior)
fn timestamp_to_naive(ts: f64) -> NaiveDateTime {
    let secs = ts.trunc() as i64;
    let nsecs = ((ts.fract()) * 1_000_000_000.0) as u32;
    DateTime::from_timestamp(secs, nsecs)
        .map(|dt| dt.naive_utc())
        .unwrap_or_else(|| Utc::now().naive_utc())
}

#[pyclass(name = "croniter")]
pub struct Croniter {
    schedule: CronSchedule,
    current_time: NaiveDateTime,
    start_time: NaiveDateTime,
    ret_type_is_float: bool,
    day_or: bool,
    is_prev: bool,
    original_expr: String,
    expressions: Vec<String>,
    tzinfo: Option<PyObject>,
}

#[pymethods]
impl Croniter {
    #[new]
    #[pyo3(signature = (expr, start_time=None, ret_type=None, day_or=true, max_years_between_matches=50, hash_id=None, implement_cron_bug=false, second_at_beginning=false, expand_from_start_time=false, is_prev=false))]
    fn new(
        py: Python<'_>,
        expr: &str,
        start_time: Option<&Bound<'_, PyAny>>,
        ret_type: Option<&Bound<'_, PyAny>>,
        day_or: bool,
        max_years_between_matches: i32,
        hash_id: Option<&str>,
        implement_cron_bug: bool,
        second_at_beginning: bool,
        expand_from_start_time: bool,
        is_prev: bool,
    ) -> PyResult<Self> {
        // Parse start_time
        let start_dt = if let Some(st) = start_time {
            if st.is_none() {
                Local::now().naive_local()
            } else {
                // Check if it's a float (timestamp)
                if let Ok(ts) = st.extract::<f64>() {
                    timestamp_to_naive(ts)
                } else {
                    py_datetime_to_naive(py, st)?
                }
            }
        } else {
            Local::now().naive_local()
        };

        // Determine return type
        let ret_type_is_float = if let Some(rt) = ret_type {
            if rt.is_none() {
                false
            } else {
                // Check if it's the float type itself
                if let Ok(name) = rt.getattr("__name__") {
                    let name_str: String = name.extract().unwrap_or_default();
                    name_str == "float"
                } else {
                    // Check if it's a float instance
                    rt.extract::<f64>().is_ok()
                }
            }
        } else {
            false
        };

        // Parse the cron expression
        let schedule = CronSchedule::parse(expr, hash_id, second_at_beginning, day_or, implement_cron_bug)
            .map_err(|e| {
                // Check if error should be CroniterNotAlphaError
                if e.starts_with("NOT_ALPHA:") {
                    let msg = e.trim_start_matches("NOT_ALPHA:").trim().to_string();
                    CroniterNotAlphaError::new_err(msg)
                } else {
                    CroniterBadCronError::new_err(e)
                }
            })?;

        // Store original expression and split into parts
        let original_expr = expr.to_string();
        let expressions: Vec<String> = expr.split_whitespace().map(|s| s.to_string()).collect();

        // Extract tzinfo if present
        let tzinfo = if let Some(st) = start_time {
            if !st.is_none() {
                st.getattr("tzinfo").ok().map(|tz| tz.unbind())
            } else {
                None
            }
        } else {
            None
        };

        Ok(Croniter {
            schedule,
            current_time: start_dt,
            start_time: start_dt,
            ret_type_is_float,
            day_or,
            is_prev,
            original_expr,
            expressions,
            tzinfo,
        })
    }

    fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
        slf
    }

    fn __next__(&mut self, py: Python<'_>) -> PyResult<Option<PyObject>> {
        if self.is_prev {
            let prev_time = self.schedule.prev_from(self.current_time);
            match prev_time {
                Some(t) => {
                    self.current_time = t;
                    Ok(Some(self.return_value_internal(py, t)?))
                }
                None => Ok(None),
            }
        } else {
            let next_time = self.schedule.next_from(self.current_time);
            match next_time {
                Some(t) => {
                    self.current_time = t;
                    Ok(Some(self.return_value_internal(py, t)?))
                }
                None => Ok(None),
            }
        }
    }

    /// Return self as iterator for forward iteration (Python croniter compatibility)
    /// Usage: for t in cron.all_next(datetime): ...
    #[pyo3(signature = (ret_type=None))]
    fn all_next(slf: PyRef<'_, Self>, py: Python<'_>, ret_type: Option<&Bound<'_, PyAny>>) -> PyResult<Py<Self>> {
        let mut new_cron = Croniter {
            schedule: slf.schedule.clone(),
            current_time: slf.current_time,
            start_time: slf.start_time,
            ret_type_is_float: slf.ret_type_is_float,
            day_or: slf.day_or,
            is_prev: false,
            original_expr: slf.original_expr.clone(),
            expressions: slf.expressions.clone(),
            tzinfo: slf.tzinfo.as_ref().map(|tz| tz.clone_ref(py)),
        };
        // Update ret_type if provided
        if let Some(rt) = ret_type {
            if !rt.is_none() {
                if let Ok(name) = rt.getattr("__name__") {
                    let name_str: String = name.extract().unwrap_or_default();
                    new_cron.ret_type_is_float = name_str == "float";
                }
            }
        }
        Py::new(py, new_cron)
    }

    /// Return self as iterator for backward iteration (Python croniter compatibility)
    /// Usage: for t in cron.all_prev(datetime): ...
    #[pyo3(signature = (ret_type=None))]
    fn all_prev(slf: PyRef<'_, Self>, py: Python<'_>, ret_type: Option<&Bound<'_, PyAny>>) -> PyResult<Py<Self>> {
        let mut new_cron = Croniter {
            schedule: slf.schedule.clone(),
            current_time: slf.current_time,
            start_time: slf.start_time,
            ret_type_is_float: slf.ret_type_is_float,
            day_or: slf.day_or,
            is_prev: true,
            original_expr: slf.original_expr.clone(),
            expressions: slf.expressions.clone(),
            tzinfo: slf.tzinfo.as_ref().map(|tz| tz.clone_ref(py)),
        };
        // Update ret_type if provided
        if let Some(rt) = ret_type {
            if !rt.is_none() {
                if let Ok(name) = rt.getattr("__name__") {
                    let name_str: String = name.extract().unwrap_or_default();
                    new_cron.ret_type_is_float = name_str == "float";
                }
            }
        }
        Py::new(py, new_cron)
    }

    /// Alias for get_next (Python croniter compatibility)
    #[pyo3(signature = (ret_type=None, start_time=None, update_current=true))]
    fn next(&mut self, py: Python<'_>, ret_type: Option<&Bound<'_, PyAny>>, start_time: Option<&Bound<'_, PyAny>>, update_current: bool) -> PyResult<PyObject> {
        self.get_next(py, ret_type, start_time, update_current)
    }

    /// Return iterator (Python croniter compatibility)
    /// Usage: for t in cron.iter(datetime): ...
    #[pyo3(signature = (ret_type=None))]
    fn iter(slf: PyRef<'_, Self>, py: Python<'_>, ret_type: Option<&Bound<'_, PyAny>>) -> PyResult<Py<Self>> {
        let mut new_cron = Croniter {
            schedule: slf.schedule.clone(),
            current_time: slf.current_time,
            start_time: slf.start_time,
            ret_type_is_float: slf.ret_type_is_float,
            day_or: slf.day_or,
            is_prev: slf.is_prev,
            original_expr: slf.original_expr.clone(),
            expressions: slf.expressions.clone(),
            tzinfo: slf.tzinfo.as_ref().map(|tz| tz.clone_ref(py)),
        };
        // Update ret_type if provided
        if let Some(rt) = ret_type {
            if !rt.is_none() {
                if let Ok(name) = rt.getattr("__name__") {
                    let name_str: String = name.extract().unwrap_or_default();
                    new_cron.ret_type_is_float = name_str == "float";
                }
            }
        }
        Py::new(py, new_cron)
    }

    #[pyo3(signature = (ret_type=None, start_time=None, update_current=true))]
    fn get_next(&mut self, py: Python<'_>, ret_type: Option<&Bound<'_, PyAny>>, start_time: Option<&Bound<'_, PyAny>>, update_current: bool) -> PyResult<PyObject> {
        // If start_time is provided, set it as current
        if let Some(st) = start_time {
            if !st.is_none() {
                self.set_current(py, st, None)?;
            }
        }

        let next_time = self.schedule.next_from(self.current_time)
            .ok_or_else(|| CroniterBadDateError::new_err("No next date found"))?;

        // Update current_time only if update_current is true
        if update_current {
            self.current_time = next_time;
        }

        self.return_value(py, next_time, ret_type)
    }

    #[pyo3(signature = (ret_type=None, start_time=None, update_current=true))]
    fn get_prev(&mut self, py: Python<'_>, ret_type: Option<&Bound<'_, PyAny>>, start_time: Option<&Bound<'_, PyAny>>, update_current: bool) -> PyResult<PyObject> {
        // If start_time is provided, set it as current
        if let Some(st) = start_time {
            if !st.is_none() {
                self.set_current(py, st, None)?;
            }
        }

        let prev_time = self.schedule.prev_from(self.current_time)
            .ok_or_else(|| CroniterBadDateError::new_err("No previous date found"))?;

        // Update current_time only if update_current is true
        if update_current {
            self.current_time = prev_time;
        }

        self.return_value(py, prev_time, ret_type)
    }

    #[pyo3(signature = (ret_type=None))]
    fn get_current(&self, py: Python<'_>, ret_type: Option<&Bound<'_, PyAny>>) -> PyResult<PyObject> {
        self.return_value(py, self.current_time, ret_type)
    }

    #[pyo3(signature = (start_time, _force=None))]
    fn set_current(&mut self, py: Python<'_>, start_time: &Bound<'_, PyAny>, _force: Option<bool>) -> PyResult<f64> {
        let dt = if let Ok(ts) = start_time.extract::<f64>() {
            timestamp_to_naive(ts)
        } else {
            py_datetime_to_naive(py, start_time)?
        };
        self.current_time = dt;
        Ok(naive_to_timestamp(dt))
    }

    #[getter]
    fn expanded(&self, py: Python<'_>) -> PyResult<PyObject> {
        let expanded = self.schedule.get_expanded();
        Ok(expanded.into_pyobject(py)?.unbind())
    }

    #[getter]
    fn tzinfo(&self, py: Python<'_>) -> PyResult<PyObject> {
        if let Some(ref tz) = self.tzinfo {
            Ok(tz.clone_ref(py))
        } else {
            Ok(py.None())
        }
    }

    #[getter]
    fn expressions(&self, py: Python<'_>) -> PyResult<PyObject> {
        Ok(self.expressions.clone().into_pyobject(py)?.unbind())
    }

    #[getter]
    fn fields(&self) -> Vec<i32> {
        // Return field indices based on whether seconds are included
        if self.expressions.len() == 6 {
            vec![0, 1, 2, 3, 4, 5]
        } else {
            vec![0, 1, 2, 3, 4]
        }
    }

    /// Convert timestamp to datetime object
    #[pyo3(signature = (timestamp, tzinfo=None))]
    fn timestamp_to_datetime(&self, py: Python<'_>, timestamp: f64, tzinfo: Option<&Bound<'_, PyAny>>) -> PyResult<PyObject> {
        let dt = timestamp_to_naive(timestamp);
        let py_dt = naive_to_py_datetime(py, dt)?;

        // Apply tzinfo if provided
        if let Some(tz) = tzinfo {
            if !tz.is_none() {
                // Call replace(tzinfo=tz) on the datetime
                let kwargs = pyo3::types::PyDict::new(py);
                kwargs.set_item("tzinfo", tz)?;
                let result = py_dt.call_method("replace", (), Some(&kwargs))?;
                return Ok(result.into_any().unbind());
            }
        }

        Ok(py_dt.into_any().unbind())
    }

    #[classmethod]
    #[pyo3(signature = (expr, hash_id=None, encoding="UTF-8", second_at_beginning=false))]
    fn is_valid(_cls: &Bound<'_, PyType>, expr: &str, hash_id: Option<&str>, encoding: Option<&str>, second_at_beginning: bool) -> bool {
        CronSchedule::parse(expr, hash_id, second_at_beginning, true, false).is_ok()
    }

    /// Class attributes for cron field ranges
    #[classattr]
    fn RANGES() -> Vec<(i32, i32)> {
        vec![(0, 59), (0, 23), (1, 31), (1, 12), (0, 6), (0, 59), (1970, 2099)]
    }

    #[classattr]
    fn MONTHS_IN_YEAR() -> i32 {
        12
    }

    #[classattr]
    fn LEN_MEANS_ALL() -> Vec<i32> {
        vec![60, 24, 31, 12, 7, 60, 130]
    }

    /// Handle value aliases for special cases
    #[classmethod]
    #[pyo3(signature = (val, field_index, _len_expressions=5))]
    fn value_alias(_cls: &Bound<'_, PyType>, val: i32, field_index: i32, _len_expressions: i32) -> i32 {
        // Handle special value mappings
        match field_index {
            2 => {
                // Day of month field
                if val == 0 {
                    1 // Day 0 becomes 1
                } else {
                    val
                }
            }
            3 => {
                // Month field
                if val == 0 {
                    1 // Month 0 becomes 1
                } else {
                    val
                }
            }
            4 => {
                // Weekday field
                if val == 7 {
                    0 // Sunday: 7 becomes 0
                } else {
                    val
                }
            }
            _ => val,
        }
    }

    /// Expand a cron expression into its component values (class method)
    /// Returns a tuple of (expanded_fields, extra_info)
    #[classmethod]
    #[pyo3(signature = (expr, hash_id=None, second_at_beginning=false))]
    fn expand(_cls: &Bound<'_, PyType>, py: Python<'_>, expr: &str, hash_id: Option<&str>, second_at_beginning: bool) -> PyResult<PyObject> {
        let schedule = CronSchedule::parse(expr, hash_id, second_at_beginning, true, false)
            .map_err(|e| {
                if e.starts_with("NOT_ALPHA:") {
                    let msg = e.trim_start_matches("NOT_ALPHA:").trim().to_string();
                    CroniterNotAlphaError::new_err(msg)
                } else {
                    CroniterBadCronError::new_err(e)
                }
            })?;
        let expanded = schedule.get_expanded();

        // Return format matching Python croniter: (expanded_list, extra_dict)
        // Python croniter returns wildcards as ['*'] but we expand them
        // For better compatibility, we return the expanded values
        let empty_dict = pyo3::types::PyDict::new(py);
        let result = (expanded, empty_dict);
        Ok(result.into_pyobject(py)?.unbind().into())
    }

    #[classmethod]
    #[pyo3(signature = (expr, dt, day_or=true))]
    fn match_(_cls: &Bound<'_, PyType>, py: Python<'_>, expr: &str, dt: &Bound<'_, PyAny>, day_or: Option<bool>) -> PyResult<bool> {
        let naive_dt = py_datetime_to_naive(py, dt)?;
        let schedule = CronSchedule::parse(expr, None, false, day_or.unwrap_or(true), false)
            .map_err(|e| {
                if e.starts_with("NOT_ALPHA:") {
                    let msg = e.trim_start_matches("NOT_ALPHA:").trim().to_string();
                    CroniterNotAlphaError::new_err(msg)
                } else {
                    CroniterBadCronError::new_err(e)
                }
            })?;

        // Python's match uses get_prev to check if the datetime matches
        // It adds 1 microsecond to the test time, calls get_prev, and checks if
        // the result is within the precision window (60 seconds for 5-field, 1 second for 6-field)
        let precision_seconds: i64 = if schedule.has_seconds() { 1 } else { 60 };

        // Add 1 microsecond (use nanoseconds since chrono doesn't have microsecond Duration)
        let test_time = naive_dt + Duration::nanoseconds(1000);

        if let Some(prev) = schedule.prev_from(test_time) {
            // Calculate difference in microseconds for precision
            let diff_micros = (test_time - prev).num_microseconds().unwrap_or(i64::MAX);
            let precision_micros = precision_seconds * 1_000_000;
            Ok(diff_micros < precision_micros)
        } else {
            Ok(false)
        }
    }

    #[staticmethod]
    #[pyo3(name = "match")]
    #[pyo3(signature = (expr, dt, day_or=true))]
    fn match_static(py: Python<'_>, expr: &str, dt: &Bound<'_, PyAny>, day_or: Option<bool>) -> PyResult<bool> {
        let naive_dt = py_datetime_to_naive(py, dt)?;
        let schedule = CronSchedule::parse(expr, None, false, day_or.unwrap_or(true), false)
            .map_err(|e| {
                if e.starts_with("NOT_ALPHA:") {
                    let msg = e.trim_start_matches("NOT_ALPHA:").trim().to_string();
                    CroniterNotAlphaError::new_err(msg)
                } else {
                    CroniterBadCronError::new_err(e)
                }
            })?;

        // Python's match uses get_prev to check if the datetime matches
        let precision_seconds: i64 = if schedule.has_seconds() { 1 } else { 60 };

        // Add 1 microsecond
        let test_time = naive_dt + Duration::nanoseconds(1000);

        if let Some(prev) = schedule.prev_from(test_time) {
            let diff_micros = (test_time - prev).num_microseconds().unwrap_or(i64::MAX);
            let precision_micros = precision_seconds * 1_000_000;
            Ok(diff_micros < precision_micros)
        } else {
            Ok(false)
        }
    }

    /// Check if there's a match within a time range
    #[staticmethod]
    #[pyo3(signature = (expr, start, end, day_or=true))]
    fn match_range(py: Python<'_>, expr: &str, start: &Bound<'_, PyAny>, end: &Bound<'_, PyAny>, day_or: Option<bool>) -> PyResult<bool> {
        let start_dt = py_datetime_to_naive(py, start)?;
        let end_dt = py_datetime_to_naive(py, end)?;

        let schedule = CronSchedule::parse(expr, None, false, day_or.unwrap_or(true), false)
            .map_err(|e| {
                if e.starts_with("NOT_ALPHA:") {
                    let msg = e.trim_start_matches("NOT_ALPHA:").trim().to_string();
                    CroniterNotAlphaError::new_err(msg)
                } else {
                    CroniterBadCronError::new_err(e)
                }
            })?;

        // Check if there's any match between start and end
        // Start from one second before start_dt to include start_dt itself
        let check_start = start_dt - Duration::seconds(1);
        if let Some(next_match) = schedule.next_from(check_start) {
            Ok(next_match >= start_dt && next_match <= end_dt)
        } else {
            Ok(false)
        }
    }

    /// Get the nth weekday of a month
    /// weekday: 0=Monday, 6=Sunday (Python weekday convention)
    /// nth: 1-5 for first through fifth occurrence
    #[classmethod]
    #[pyo3(signature = (year, month, weekday, nth))]
    fn _get_nth_weekday_of_month(_cls: &Bound<'_, PyType>, py: Python<'_>, year: i32, month: u32, weekday: u32, nth: u32) -> PyResult<PyObject> {
        // Convert Python weekday (0=Mon) to chrono weekday
        let target_weekday = match weekday {
            0 => Weekday::Mon,
            1 => Weekday::Tue,
            2 => Weekday::Wed,
            3 => Weekday::Thu,
            4 => Weekday::Fri,
            5 => Weekday::Sat,
            6 => Weekday::Sun,
            _ => return Err(PyValueError::new_err("Invalid weekday")),
        };

        // Find the first day of the month
        let first_day = chrono::NaiveDate::from_ymd_opt(year, month, 1)
            .ok_or_else(|| PyValueError::new_err("Invalid date"))?;

        // Find the first occurrence of the target weekday
        let mut current = first_day;
        while current.weekday() != target_weekday {
            current = current.succ_opt().ok_or_else(|| PyValueError::new_err("Date overflow"))?;
        }

        // Move to the nth occurrence
        if nth > 1 {
            current = current + Duration::days(((nth - 1) * 7) as i64);
        }

        // Check if we're still in the same month
        if current.month() != month {
            return Ok(py.None());
        }

        Ok(current.day().into_pyobject(py)?.unbind().into())
    }

    /// Get multiple next occurrences
    #[pyo3(signature = (n, ret_type=None))]
    fn get_next_n(&mut self, py: Python<'_>, n: usize, ret_type: Option<&Bound<'_, PyAny>>) -> PyResult<PyObject> {
        let mut results = Vec::new();
        for _ in 0..n {
            let next_time = self.schedule.next_from(self.current_time)
                .ok_or_else(|| CroniterBadDateError::new_err("No next date found"))?;
            self.current_time = next_time;
            results.push(self.return_value(py, next_time, ret_type)?);
        }
        Ok(results.into_pyobject(py)?.unbind())
    }

    /// Get multiple previous occurrences
    #[pyo3(signature = (n, ret_type=None))]
    fn get_prev_n(&mut self, py: Python<'_>, n: usize, ret_type: Option<&Bound<'_, PyAny>>) -> PyResult<PyObject> {
        let mut results = Vec::new();
        for _ in 0..n {
            let prev_time = self.schedule.prev_from(self.current_time)
                .ok_or_else(|| CroniterBadDateError::new_err("No previous date found"))?;
            self.current_time = prev_time;
            results.push(self.return_value(py, prev_time, ret_type)?);
        }
        Ok(results.into_pyobject(py)?.unbind())
    }
}

impl Croniter {
    fn return_value(&self, py: Python<'_>, dt: NaiveDateTime, ret_type: Option<&Bound<'_, PyAny>>) -> PyResult<PyObject> {
        // Determine if we should return float or datetime
        let return_float = if let Some(rt) = ret_type {
            if rt.is_none() {
                self.ret_type_is_float
            } else {
                let type_name = rt.get_type().name()?;
                let type_name_str = type_name.to_string();
                if type_name_str.contains("float") || type_name_str == "type" {
                    // Check if it's the float type itself
                    if let Ok(name) = rt.getattr("__name__") {
                        let name_str: String = name.extract()?;
                        name_str == "float"
                    } else {
                        type_name_str.contains("float")
                    }
                } else {
                    false
                }
            }
        } else {
            self.ret_type_is_float
        };

        if return_float {
            Ok(naive_to_timestamp(dt).into_pyobject(py)?.unbind().into())
        } else {
            Ok(naive_to_py_datetime(py, dt)?.into_any().unbind())
        }
    }

    fn return_value_internal(&self, py: Python<'_>, dt: NaiveDateTime) -> PyResult<PyObject> {
        if self.ret_type_is_float {
            Ok(naive_to_timestamp(dt).into_pyobject(py)?.unbind().into())
        } else {
            Ok(naive_to_py_datetime(py, dt)?.into_any().unbind())
        }
    }
}

/// Helper function to convert datetime to timestamp
#[pyfunction]
fn datetime_to_timestamp(py: Python<'_>, dt: &Bound<'_, PyAny>) -> PyResult<f64> {
    let naive = py_datetime_to_naive(py, dt)?;
    Ok(naive_to_timestamp(naive))
}

/// Iterator for croniter_range
#[pyclass]
struct CroniterRangeIterator {
    cron: Croniter,
    stop: NaiveDateTime,
    forward: bool,
    ret_type_is_float: bool,
    exclude_ends: bool,
    started: bool,
}

#[pymethods]
impl CroniterRangeIterator {
    fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
        slf
    }

    fn __next__(&mut self, py: Python<'_>) -> PyResult<Option<PyObject>> {
        let result = if self.forward {
            self.cron.schedule.next_from(self.cron.current_time)
        } else {
            self.cron.schedule.prev_from(self.cron.current_time)
        };

        match result {
            Some(dt) => {
                let in_range = if self.forward {
                    dt < self.stop || (!self.exclude_ends && dt == self.stop)
                } else {
                    dt > self.stop || (!self.exclude_ends && dt == self.stop)
                };

                if in_range {
                    self.cron.current_time = dt;
                    if self.ret_type_is_float {
                        Ok(Some(naive_to_timestamp(dt).into_pyobject(py)?.unbind().into()))
                    } else {
                        Ok(Some(naive_to_py_datetime(py, dt)?.into_any().unbind()))
                    }
                } else {
                    Ok(None)
                }
            }
            None => Ok(None),
        }
    }
}

/// croniter_range function - generates all matching times between start and stop
#[pyfunction]
#[pyo3(signature = (start, stop, expr_format, ret_type=None, day_or=true, exclude_ends=false, _croniter=None, second_at_beginning=false, expand_from_start_time=false))]
fn croniter_range(
    py: Python<'_>,
    start: &Bound<'_, PyAny>,
    stop: &Bound<'_, PyAny>,
    expr_format: &str,
    ret_type: Option<&Bound<'_, PyAny>>,
    day_or: bool,
    exclude_ends: bool,
    _croniter: Option<&Bound<'_, PyAny>>,
    second_at_beginning: bool,
    expand_from_start_time: bool,
) -> PyResult<CroniterRangeIterator> {
    let start_dt = py_datetime_to_naive(py, start)?;
    let stop_dt = py_datetime_to_naive(py, stop)?;

    let forward = start_dt < stop_dt;

    // Determine return type
    let ret_type_is_float = if let Some(rt) = ret_type {
        if rt.is_none() {
            false
        } else if let Ok(name) = rt.getattr("__name__") {
            let name_str: String = name.extract().unwrap_or_default();
            name_str == "float"
        } else {
            false
        }
    } else {
        false
    };

    // Adjust start time for exclude_ends
    let adjusted_start = if !exclude_ends {
        if forward {
            start_dt - Duration::microseconds(1)
        } else {
            start_dt + Duration::microseconds(1)
        }
    } else {
        start_dt
    };

    let schedule = CronSchedule::parse(expr_format, None, second_at_beginning, day_or, false)
        .map_err(|e| {
            if e.starts_with("NOT_ALPHA:") {
                let msg = e.trim_start_matches("NOT_ALPHA:").trim().to_string();
                CroniterNotAlphaError::new_err(msg)
            } else {
                CroniterBadCronError::new_err(e)
            }
        })?;

    let cron = Croniter {
        schedule,
        current_time: adjusted_start,
        start_time: adjusted_start,
        ret_type_is_float,
        day_or,
        is_prev: !forward,
        original_expr: expr_format.to_string(),
        expressions: expr_format.split_whitespace().map(|s| s.to_string()).collect(),
        tzinfo: None,
    };

    Ok(CroniterRangeIterator {
        cron,
        stop: stop_dt,
        forward,
        ret_type_is_float,
        exclude_ends,
        started: false,
    })
}

/// A Python module implemented in Rust.
#[pymodule]
fn croniter_rs(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<Croniter>()?;
    m.add_class::<CroniterRangeIterator>()?;
    m.add_function(wrap_pyfunction!(datetime_to_timestamp, m)?)?;
    m.add_function(wrap_pyfunction!(croniter_range, m)?)?;

    // Field index constants
    m.add("MINUTE_FIELD", MINUTE_FIELD)?;
    m.add("HOUR_FIELD", HOUR_FIELD)?;
    m.add("DAY_FIELD", DAY_FIELD)?;
    m.add("MONTH_FIELD", MONTH_FIELD)?;
    m.add("DOW_FIELD", DOW_FIELD)?;
    m.add("SECOND_FIELD", SECOND_FIELD)?;
    m.add("YEAR_FIELD", YEAR_FIELD)?;

    // Exception types
    m.add("CroniterError", py.get_type::<CroniterError>())?;
    m.add("CroniterBadCronError", py.get_type::<CroniterBadCronError>())?;
    m.add("CroniterBadDateError", py.get_type::<CroniterBadDateError>())?;
    m.add("CroniterNotAlphaError", py.get_type::<CroniterNotAlphaError>())?;
    m.add("CroniterUnsupportedSyntaxError", py.get_type::<CroniterUnsupportedSyntaxError>())?;
    m.add("CroniterBadTypeRangeError", py.get_type::<CroniterBadTypeRangeError>())?;

    // UTC_DT constant
    let datetime_mod = py.import("datetime")?;
    let timezone = datetime_mod.getattr("timezone")?;
    let utc = timezone.getattr("utc")?;
    m.add("UTC_DT", utc)?;

    // OVERFLOW32B_MODE - false on 64-bit systems
    m.add("OVERFLOW32B_MODE", false)?;

    Ok(())
}
