use std::collections::HashSet;
use crc32fast::Hasher;

/// Represents a parsed cron expression
#[derive(Debug, Clone)]
pub struct CronExpr {
    pub seconds: Vec<u32>,
    pub minutes: Vec<u32>,
    pub hours: Vec<u32>,
    pub days: Vec<u32>,
    pub months: Vec<u32>,
    pub weekdays: Vec<u32>,
    pub years: Vec<u32>,
    pub nth_weekdays: Vec<(u32, u32)>, // (weekday, nth) - nth=0 means last occurrence
    pub last_day: bool,
    pub has_seconds: bool,
    pub has_years: bool,
    // Track if original expression was "*" (for Python-compatible day_or logic)
    pub day_is_star: bool,
    pub weekday_is_star: bool,
    // Track if original expression starts with "*" (for implement_cron_bug logic)
    pub day_starts_with_star: bool,
    pub weekday_starts_with_star: bool,
}

// Day name mappings
const DAY_NAMES: &[(&str, u32)] = &[
    ("sun", 0), ("mon", 1), ("tue", 2), ("wed", 3),
    ("thu", 4), ("fri", 5), ("sat", 6),
    ("sunday", 0), ("monday", 1), ("tuesday", 2), ("wednesday", 3),
    ("thursday", 4), ("friday", 5), ("saturday", 6),
];

// Month name mappings
const MONTH_NAMES: &[(&str, u32)] = &[
    ("jan", 1), ("feb", 2), ("mar", 3), ("apr", 4),
    ("may", 5), ("jun", 6), ("jul", 7), ("aug", 8),
    ("sep", 9), ("oct", 10), ("nov", 11), ("dec", 12),
    ("january", 1), ("february", 2), ("march", 3), ("april", 4),
    ("june", 6), ("july", 7), ("august", 8),
    ("september", 9), ("october", 10), ("november", 11), ("december", 12),
];

// Keyword expression mappings (cron shortcuts)
// Format: (keyword, expansion_without_hash, expansion_with_hash)
const KEYWORD_EXPRESSIONS: &[(&str, &str, &str)] = &[
    ("@yearly", "0 0 1 1 *", "h h h h * h"),
    ("@annually", "0 0 1 1 *", "h h h h * h"),
    ("@monthly", "0 0 1 * *", "h h h * * h"),
    ("@weekly", "0 0 * * 0", "h h * * h h"),
    ("@daily", "0 0 * * *", "h h * * * h"),
    ("@midnight", "0 0 * * *", "h h(0-2) * * * h"),
    ("@hourly", "0 * * * *", "h * * * * h"),
];

impl CronExpr {
    pub fn parse(expr: &str, hash_id: Option<&str>, second_at_beginning: bool) -> Result<Self, String> {
        // Handle keyword expressions like @daily, @hourly, etc.
        let expr = expr.trim();
        let normalized_expr = if expr.starts_with('@') {
            let keyword = expr.to_lowercase();
            let mut found = None;
            for (kw, expansion_no_hash, expansion_with_hash) in KEYWORD_EXPRESSIONS {
                if keyword == *kw {
                    // Use hash expansion if hash_id is provided
                    found = Some(if hash_id.is_some() { *expansion_with_hash } else { *expansion_no_hash });
                    break;
                }
            }
            match found {
                Some(expanded) => expanded.to_string(),
                None => {
                    // @reboot is not supported (it's a special case for system startup)
                    if keyword == "@reboot" {
                        return Err("@reboot is not supported (system startup trigger)".to_string());
                    }
                    return Err(format!("Unknown keyword expression: {}", expr));
                }
            }
        } else {
            expr.to_string()
        };

        let parts: Vec<&str> = normalized_expr.split_whitespace().collect();

        let (seconds, minutes, hours, days, months, weekdays, years, has_seconds, has_years) = match parts.len() {
            5 => {
                // Standard 5-field cron: minute hour day month weekday
                let minutes = parse_field(parts[0], 0, 59, None, hash_id, false, 0)?;
                let hours = parse_field(parts[1], 0, 23, None, hash_id, false, 1)?;
                let (days, last_day) = parse_day_field(parts[2], hash_id, false)?; // 5-field: don't allow day 0
                let months = parse_field(parts[3], 1, 12, Some(&MONTH_NAMES), hash_id, false, 3)?;
                let weekdays = parse_weekday_field(parts[4], hash_id)?;
                let years = (1970..=2099).collect();
                (vec![0], minutes, hours, days, months, weekdays, years, false, false)
            }
            6 => {
                if second_at_beginning {
                    // 6-field with second at beginning: second minute hour day month weekday
                    let seconds = parse_field(parts[0], 0, 59, None, hash_id, false, 5)?;
                    let minutes = parse_field(parts[1], 0, 59, None, hash_id, false, 0)?;
                    let hours = parse_field(parts[2], 0, 23, None, hash_id, false, 1)?;
                    let (days, _) = parse_day_field(parts[3], hash_id, true)?; // 6-field: allow day 0 -> 1
                    let months = parse_field(parts[4], 1, 12, Some(&MONTH_NAMES), hash_id, false, 3)?;
                    let weekdays = parse_weekday_field(parts[5], hash_id)?;
                    let years = (1970..=2099).collect();
                    (seconds, minutes, hours, days, months, weekdays, years, true, false)
                } else {
                    // 6-field with second at end: minute hour day month weekday second
                    let minutes = parse_field(parts[0], 0, 59, None, hash_id, false, 0)?;
                    let hours = parse_field(parts[1], 0, 23, None, hash_id, false, 1)?;
                    let (days, _) = parse_day_field(parts[2], hash_id, true)?; // 6-field: allow day 0 -> 1
                    let months = parse_field(parts[3], 1, 12, Some(&MONTH_NAMES), hash_id, false, 3)?;
                    let weekdays = parse_weekday_field(parts[4], hash_id)?;
                    let seconds = parse_field(parts[5], 0, 59, None, hash_id, false, 5)?;
                    let years = (1970..=2099).collect();
                    (seconds, minutes, hours, days, months, weekdays, years, true, false)
                }
            }
            7 => {
                // 7-field format: minute hour day month weekday second year
                if second_at_beginning {
                    // second minute hour day month weekday year
                    let seconds = parse_field(parts[0], 0, 59, None, hash_id, false, 5)?;
                    let minutes = parse_field(parts[1], 0, 59, None, hash_id, false, 0)?;
                    let hours = parse_field(parts[2], 0, 23, None, hash_id, false, 1)?;
                    let (days, _) = parse_day_field(parts[3], hash_id, true)?; // 7-field: allow day 0 -> 1
                    let months = parse_field(parts[4], 1, 12, Some(&MONTH_NAMES), hash_id, false, 3)?;
                    let weekdays = parse_weekday_field(parts[5], hash_id)?;
                    let years = parse_field(parts[6], 1970, 2099, None, hash_id, false, 6)?;
                    (seconds, minutes, hours, days, months, weekdays, years, true, true)
                } else {
                    // minute hour day month weekday second year
                    let minutes = parse_field(parts[0], 0, 59, None, hash_id, false, 0)?;
                    let hours = parse_field(parts[1], 0, 23, None, hash_id, false, 1)?;
                    let (days, _) = parse_day_field(parts[2], hash_id, true)?; // 7-field: allow day 0 -> 1
                    let months = parse_field(parts[3], 1, 12, Some(&MONTH_NAMES), hash_id, false, 3)?;
                    let weekdays = parse_weekday_field(parts[4], hash_id)?;
                    let seconds = parse_field(parts[5], 0, 59, None, hash_id, false, 5)?;
                    let years = parse_field(parts[6], 1970, 2099, None, hash_id, false, 6)?;
                    (seconds, minutes, hours, days, months, weekdays, years, true, true)
                }
            }
            _ => return Err(format!("Invalid cron expression: expected 5, 6, or 7 fields, got {}", parts.len())),
        };

        // Re-parse to get last_day and nth_weekdays
        let day_part = match parts.len() {
            5 => parts[2],
            6 => if second_at_beginning { parts[3] } else { parts[2] },
            7 => if second_at_beginning { parts[3] } else { parts[2] },
            _ => parts[2],
        };
        let weekday_part = match parts.len() {
            5 => parts[4],
            6 => if second_at_beginning { parts[5] } else { parts[4] },
            7 => if second_at_beginning { parts[5] } else { parts[4] },
            _ => parts[4],
        };

        let allow_day_zero = parts.len() >= 6; // 6 or 7 field formats allow day 0
        let (_, last_day) = parse_day_field(day_part, hash_id, allow_day_zero)?;
        let nth_weekdays = parse_nth_weekdays(weekday_part)?;

        // Check if original expressions were "*" or "?" (Python compatibility)
        // Python's optimization: when DAY field CONTAINS "*" (like "*" or "*/3"),
        // weekday expressions covering all days (like "sun-sat", "0-6") are optimized to ['*']
        // But when DAY field does NOT contain "*" (like "1,4,7"), they remain as [0,1,2,3,4,5,6]
        // This affects the day_or split condition
        //
        // IMPORTANT: Python's expanded array behavior:
        // - Pure "*" or "?" -> expanded = ['*'] (string)
        // - "*/3" -> expanded = [0, 3, 6] (numbers, NOT ['*'])
        // - "5-5" (X-X range) -> expanded = ['*'] ONLY if the OTHER field starts with '*'
        //
        // The condition for OR logic is: expanded[DAY][0] != '*' AND expanded[DOW][0] != '*'
        // This means we check if the FIRST ELEMENT of expanded is the STRING '*'
        //
        // So:
        // - day="*" -> day_is_star=true (expanded=['*'])
        // - day="*/3" -> day_is_star=false (expanded=[1,4,7,...], first element is number)
        // - day="5-5" with weekday="*" -> day_is_star=true (expanded=['*'])
        // - day="5-5" with weekday="1" -> day_is_star=false (expanded=[1,2,...,31])

        let weekday_starts_with_star = weekday_part.trim().starts_with('*');
        let day_starts_with_star = day_part.trim().starts_with('*');

        // Pure star or question mark
        let weekday_is_pure_star = weekday_part.trim() == "*" || weekday_part.trim() == "?";
        let day_is_pure_star = day_part.trim() == "*" || day_part.trim() == "?";

        // weekday_contains_star: used for day optimization
        let weekday_contains_star = weekday_part.contains('*') || weekday_part.trim() == "?";

        // day_is_star: true if day is pure "*"/"?", or X-X range when weekday contains "*",
        // OR if weekday contains "*" and day covers all 31 days (like "1-31")
        // Note: "*/3" is NOT considered as star (it expands to numbers, not ['*'])
        //
        // Python logic (from croniter.py lines 1040-1048):
        // if len(res) == LEN_MEANS_ALL[field_index]:  # 31 for day field
        //     if (field_index == DAY_FIELD and "*" not in expressions[DOW_FIELD]):
        //         pass  # don't convert to ['*']
        //     else:
        //         res = ["*"]
        let day_is_star = day_is_pure_star ||
            (weekday_contains_star && is_x_x_range(day_part)) ||
            (weekday_contains_star && days.len() == 31);

        // day_contains_star: used for weekday optimization (when day contains *, weekday 0-6 becomes ['*'])
        let day_contains_star = day_part.contains('*') || day_part.trim() == "?";

        // weekday_is_star: true if weekday is pure "*"/"?", X-X range when day starts with "*",
        // OR if DAY contains "*" and weekday covers all days (0-6) without nth_weekday,
        // OR if DAY contains "*" and weekday is X-X range (like fri-fri)
        // Note: "*/3" is NOT considered as star
        let weekday_is_star = weekday_is_pure_star ||
            (day_starts_with_star && is_x_x_weekday_range(weekday_part)) ||
            (day_contains_star && is_x_x_weekday_range(weekday_part)) ||
            (day_contains_star &&
             weekdays.len() == 7 &&
             weekdays.iter().enumerate().all(|(i, &v)| v == i as u32) &&
             nth_weekdays.is_empty());

        Ok(CronExpr {
            seconds,
            minutes,
            hours,
            days,
            months,
            weekdays,
            years,
            nth_weekdays,
            last_day,
            has_seconds,
            has_years,
            day_is_star,
            weekday_is_star,
            day_starts_with_star,
            weekday_starts_with_star,
        })
    }
}

/// Parse a standard cron field (minute, hour, month, etc.)
/// allow_l: whether to allow 'L' as a value (only valid in day field)
/// field_index: the field index for hash calculation (0=minute, 1=hour, 2=day, 3=month, 4=weekday, 5=second, 6=year)
fn parse_field(
    field: &str,
    min: u32,
    max: u32,
    name_map: Option<&[(&str, u32)]>,
    hash_id: Option<&str>,
    allow_l: bool,
    field_index: usize,
) -> Result<Vec<u32>, String> {
    let mut values: HashSet<u32> = HashSet::new();

    for part in field.split(',') {
        let part = part.trim();
        if part.is_empty() {
            continue;
        }

        // Handle ? placeholder - but this should only be used in day/weekday fields
        // This function is also used for minute, hour, month, etc., so we reject ? here
        if part == "?" {
            return Err("? placeholder can only be used in day-of-month or day-of-week fields".to_string());
        }

        // Handle hash expressions like H or H(0-30)
        if part.starts_with('H') || part.starts_with('h') {
            let hash_values = parse_hash_field(part, min, max, hash_id, field_index)?;
            values.extend(hash_values);
            continue;
        }

        // Handle random expressions like R or R(0-30)
        if part.starts_with('R') || part.starts_with('r') {
            let random_values = parse_random_field(part, min, max)?;
            values.extend(random_values);
            continue;
        }

        // Handle step expressions like */5 or 1-10/2
        if part.contains('/') {
            let step_values = parse_step(part, min, max, name_map, hash_id, allow_l, field_index)?;
            values.extend(step_values);
            continue;
        }

        // Handle range expressions like 1-5
        if part.contains('-') {
            let range_values = parse_range(part, min, max, name_map, allow_l)?;
            values.extend(range_values);
            continue;
        }

        // Handle wildcard
        if part == "*" {
            for v in min..=max {
                values.insert(v);
            }
            continue;
        }

        // Check for standalone 'L' - only allowed in day field
        if part.to_lowercase() == "l" {
            if allow_l {
                // L is handled specially in parse_day_field, not here
                // But if we get here, it means L was used in a non-day field
                return Err("'L' is only allowed in day-of-month field".to_string());
            } else {
                return Err("'L' is not allowed in this field".to_string());
            }
        }

        // Handle single value (possibly with name)
        let value = parse_single_value(part, min, max, name_map, allow_l)?;
        values.insert(value);
    }

    let mut result: Vec<u32> = values.into_iter().collect();
    result.sort();
    Ok(result)
}

/// Parse a hash field like H, H(0-30), H/15, or H(30-59)/10
/// Matches Python croniter's behavior using CRC32
fn parse_hash_field(field: &str, min: u32, max: u32, hash_id: Option<&str>, field_index: usize) -> Result<Vec<u32>, String> {
    if hash_id.is_none() {
        return Err("Hash expressions (H) require hash_id to be provided".to_string());
    }

    let hash_id = hash_id.unwrap();

    // Calculate CRC32 hash (matching Python's binascii.crc32)
    let mut hasher = Hasher::new();
    hasher.update(hash_id.as_bytes());
    let crc = hasher.finalize();

    // Parse the hash expression using regex-like logic
    // Supported formats: H, H(min-max), H/divisor, H(min-max)/divisor

    let field_lower = field.to_lowercase();

    // Check for H(min-max)/divisor
    if let Some(paren_start) = field_lower.find('(') {
        if let Some(paren_end) = field_lower.find(')') {
            if let Some(slash_pos) = field_lower.find('/') {
                // H(min-max)/divisor format
                let range_str = &field_lower[paren_start+1..paren_end];
                let divisor_str = &field_lower[slash_pos+1..];

                if let Some(dash_pos) = range_str.find('-') {
                    let range_begin: u32 = range_str[..dash_pos].parse()
                        .map_err(|_| format!("Invalid range begin in: {}", field))?;
                    let range_end: u32 = range_str[dash_pos+1..].parse()
                        .map_err(|_| format!("Invalid range end in: {}", field))?;
                    let divisor: u32 = divisor_str.parse()
                        .map_err(|_| format!("Invalid divisor in: {}", field))?;

                    if divisor == 0 {
                        return Err(format!("Bad expression: {}", field));
                    }
                    if range_begin >= range_end {
                        return Err("Range end must be greater than range begin".to_string());
                    }

                    // Python logic: hash within [range_begin, range_begin+divisor-1], then step to range_end
                    let hash_range_end = divisor - 1 + range_begin;
                    let x = ((crc >> field_index) % (hash_range_end - range_begin + 1)) + range_begin;

                    // Generate x-range_end/divisor
                    let mut values = Vec::new();
                    let mut v = x;
                    while v <= range_end {
                        values.push(v);
                        v += divisor;
                    }
                    return Ok(values);
                }
            } else {
                // H(min-max) format (no divisor)
                let range_str = &field_lower[paren_start+1..paren_end];
                if let Some(dash_pos) = range_str.find('-') {
                    let range_begin: u32 = range_str[..dash_pos].parse()
                        .map_err(|_| format!("Invalid range begin in: {}", field))?;
                    let range_end: u32 = range_str[dash_pos+1..].parse()
                        .map_err(|_| format!("Invalid range end in: {}", field))?;

                    if range_begin >= range_end {
                        return Err("Range end must be greater than range begin".to_string());
                    }

                    let value = ((crc >> field_index) % (range_end - range_begin + 1)) + range_begin;
                    return Ok(vec![value]);
                }
            }
        }
    }

    // Check for H/divisor (no range)
    if let Some(slash_pos) = field_lower.find('/') {
        let divisor_str = &field_lower[slash_pos+1..];
        let divisor: u32 = divisor_str.parse()
            .map_err(|_| format!("Invalid divisor in: {}", field))?;

        if divisor == 0 {
            return Err(format!("Bad expression: {}", field));
        }

        // Python logic: hash within [min, min+divisor-1], then step to max
        let hash_range_end = divisor - 1 + min;
        let x = ((crc >> field_index) % (hash_range_end - min + 1)) + min;

        // Generate x-max/divisor
        let mut values = Vec::new();
        let mut v = x;
        while v <= max {
            values.push(v);
            v += divisor;
        }
        return Ok(values);
    }

    // Simple H - pick a value in range [min, max]
    let value = ((crc >> field_index) % (max - min + 1)) + min;
    Ok(vec![value])
}

/// Parse a random field like R or R(0-30)
fn parse_random_field(field: &str, min: u32, max: u32) -> Result<Vec<u32>, String> {
    use std::collections::hash_map::RandomState;
    use std::hash::{BuildHasher, Hash, Hasher};

    // Generate a random value using a simple method
    // Note: This is not cryptographically secure, but sufficient for cron scheduling
    let random_value = {
        let state = RandomState::new();
        let mut hasher = state.build_hasher();
        // Use current time as seed for randomness
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
            .hash(&mut hasher);
        field.hash(&mut hasher);
        hasher.finish() as u32
    };

    // Check for R(min-max) syntax
    if field.contains('(') && field.contains(')') {
        let start = field.find('(').unwrap();
        let end = field.find(')').unwrap();
        let range_str = &field[start+1..end];

        if range_str.contains('-') {
            let parts: Vec<&str> = range_str.split('-').collect();
            if parts.len() == 2 {
                let range_min: u32 = parts[0].parse().map_err(|_| format!("Invalid random range: {}", field))?;
                let range_max: u32 = parts[1].parse().map_err(|_| format!("Invalid random range: {}", field))?;
                let value = range_min + (random_value % (range_max - range_min + 1));
                return Ok(vec![value]);
            }
        }
    }

    // Check for R/step syntax
    if field.contains('/') {
        let parts: Vec<&str> = field.split('/').collect();
        if parts.len() == 2 {
            let step: u32 = parts[1].parse().map_err(|_| format!("Invalid step: {}", parts[1]))?;
            let start = random_value % step;
            let mut values = Vec::new();
            let mut v = start;
            while v <= max {
                if v >= min {
                    values.push(v);
                }
                v += step;
            }
            return Ok(values);
        }
    }

    // Simple R - pick a random value in range
    let value = min + (random_value % (max - min + 1));
    Ok(vec![value])
}

/// Parse a step expression like */5 or 1-10/2
fn parse_step(field: &str, min: u32, max: u32, name_map: Option<&[(&str, u32)]>, hash_id: Option<&str>, allow_l: bool, field_index: usize) -> Result<Vec<u32>, String> {
    let parts: Vec<&str> = field.split('/').collect();
    if parts.len() != 2 {
        return Err(format!("Invalid step expression: {}", field));
    }

    let step_str = parts[1].trim();
    if step_str.is_empty() {
        return Err(format!("Invalid step expression: {}", field));
    }
    let step: u32 = step_str.parse().map_err(|_| format!("Invalid step value: {}", parts[1]))?;
    if step == 0 {
        return Err(format!("Step cannot be zero: {}", field));
    }

    let (start, end, is_wrap) = if parts[0] == "*" {
        (min, max, false)
    } else if parts[0].contains('-') {
        let range_parts: Vec<&str> = parts[0].split('-').collect();
        if range_parts.len() != 2 {
            return Err(format!("Invalid range in step: {}", field));
        }
        let start = parse_single_value(range_parts[0], min, max, name_map, allow_l)?;
        let end = parse_single_value(range_parts[1], min, max, name_map, allow_l)?;
        // Python behavior: X-X means the whole cycle
        if start == end {
            (min, max, false)
        } else if start > end {
            // Wrap-around range (e.g., oct-jan = 10-1)
            (start, end, true)
        } else {
            (start, end, false)
        }
    } else if parts[0].starts_with('H') || parts[0].starts_with('h') {
        // Handle H/step - already handled in parse_hash_field
        return Err(format!("Hash step should be handled by parse_hash_field: {}", field));
    } else if parts[0].starts_with('R') || parts[0].starts_with('r') {
        // Handle R/step - already handled in parse_random_field
        return Err(format!("Random step should be handled by parse_random_field: {}", field));
    } else {
        // Single value with step like 4/6 means start at 4, step by 6
        let start = parse_single_value(parts[0], min, max, name_map, allow_l)?;
        (start, max, false)
    };

    let mut values = Vec::new();

    if is_wrap {
        // Handle wrap-around range with step (e.g., oct-jan/3 = 10-1/3)
        // Python behavior:
        // 1. First part: from start to max with step
        // 2. Calculate to_skip to maintain step continuity across wrap
        // 3. Second part: from min + to_skip to end with step

        // First part: from start to max with step
        let mut v = start;
        while v <= max {
            values.push(v);
            v += step;
        }

        // Calculate to_skip (Python's logic)
        // already_skipped = how many positions from last_val to max (0-indexed from end)
        // curpos = 0-indexed position of last_val
        // If stepping from curpos would go past the end, and we haven't skipped enough,
        // then skip some values at the beginning of the second part
        let to_skip = if !values.is_empty() {
            let last_val = *values.last().unwrap();
            // curpos is 0-indexed position in the range [min, max]
            let curpos = (last_val - min) as usize;
            let range_len = (max - min + 1) as usize;

            // already_skipped = distance from last_val to max
            let already_skipped = (max - last_val) as usize;

            if (curpos + step as usize) > range_len && already_skipped < step as usize {
                step - already_skipped as u32
            } else {
                0
            }
        } else {
            0
        };

        // Second part: from min + to_skip to end with step
        let second_start = min + to_skip;
        if second_start <= end {
            let mut v = second_start;
            while v <= end {
                values.push(v);
                v += step;
            }
        }
    } else {
        // Normal range
        let mut v = start;
        while v <= end {
            values.push(v);
            v += step;
        }
    }

    Ok(values)
}

/// Parse a range expression like 1-5
fn parse_range(field: &str, min: u32, max: u32, name_map: Option<&[(&str, u32)]>, allow_l: bool) -> Result<Vec<u32>, String> {
    let parts: Vec<&str> = field.split('-').collect();
    if parts.len() != 2 {
        return Err(format!("Invalid range: {}", field));
    }

    let start = parse_single_value(parts[0], min, max, name_map, allow_l)?;
    let end = parse_single_value(parts[1], min, max, name_map, allow_l)?;

    if start == end {
        // Python behavior: X-X means the whole cycle (all values)
        // e.g., "21-21" for hours means all hours, "sun-sun" means all weekdays
        Ok((min..=max).collect())
    } else if start > end {
        // Wrap around (e.g., sun-thu = 0-4 for weekdays)
        let mut values: Vec<u32> = (start..=max).collect();
        values.extend(min..=end);
        Ok(values)
    } else {
        Ok((start..=end).collect())
    }
}

/// Parse a single value, possibly a name
/// Parse a single value, possibly a name
/// allow_l: whether to allow 'L' as a value (only valid in day field)
fn parse_single_value(value: &str, min: u32, max: u32, name_map: Option<&[(&str, u32)]>, allow_l: bool) -> Result<u32, String> {
    let value = value.trim().to_lowercase();

    // Check for L (last day) - only allowed in day field
    if value == "l" {
        if allow_l {
            return Ok(max);
        } else {
            return Err(format!("'L' is not allowed in this field"));
        }
    }

    // Try name lookup first
    if let Some(names) = name_map {
        for (name, num) in names {
            if value == *name {
                return Ok(*num);
            }
        }
    }

    // Try parsing as number
    let num: u32 = value.parse().map_err(|_| format!("Invalid value: {}", value))?;

    if num < min || num > max {
        return Err(format!("Value {} out of range [{}, {}]", num, min, max));
    }

    Ok(num)
}

/// Parse day field, handling L for last day and ? placeholder
/// allow_day_zero: if true (6/7-field format), convert day 0 to day 1 (Python compatibility)
fn parse_day_field(field: &str, hash_id: Option<&str>, allow_day_zero: bool) -> Result<(Vec<u32>, bool), String> {
    let field_lower = field.to_lowercase();
    let mut last_day = false;
    let mut values: HashSet<u32> = HashSet::new();

    for part in field.split(',') {
        let part = part.trim().to_lowercase();
        if part.is_empty() {
            continue;
        }

        // Handle ? placeholder (treat as *)
        if part == "?" {
            for v in 1..=31 {
                values.insert(v);
            }
            continue;
        }

        if part == "l" {
            last_day = true;
            continue;
        }

        // Handle hash expressions
        if part.starts_with('h') {
            let hash_values = parse_hash_field(&part, 1, 31, hash_id, 2)?;
            values.extend(hash_values);
            continue;
        }

        // Handle random expressions
        if part.starts_with('r') {
            let random_values = parse_random_field(&part, 1, 31)?;
            values.extend(random_values);
            continue;
        }

        // Handle range with L like 29-L
        // Python croniter REJECTS L-N patterns (L on left side) but ACCEPTS N-L (L on right side)
        // L on right side means 31 (max day), NOT "last day of month" semantics
        // Only standalone "L" sets the last_day flag
        if part.contains('-') && part.contains('l') {
            let range_parts: Vec<&str> = part.split('-').collect();
            if range_parts.len() == 2 {
                // Python rejects L-N patterns (L on left side of range)
                if range_parts[0].trim() == "l" {
                    return Err(format!("Invalid day range '{}': L cannot be on left side of range", part));
                }

                let start: u32 = range_parts[0].parse().map_err(|_| format!("Invalid day: {}", range_parts[0]))?;
                let end: u32 = if range_parts[1].trim() == "l" {
                    31
                } else {
                    range_parts[1].parse().map_err(|_| format!("Invalid day: {}", range_parts[1]))?
                };

                // Python behavior: X-X means the whole cycle (all values)
                if start == end {
                    for v in 1..=31 {
                        values.insert(v);
                    }
                } else {
                    for v in start..=end {
                        values.insert(v);
                    }
                }
                continue;
            }
        }

        // Handle step expressions
        if part.contains('/') {
            let step_values = parse_step(&part, 1, 31, None, hash_id, true, 2)?;
            values.extend(step_values);
            continue;
        }

        // Handle range expressions
        if part.contains('-') {
            let range_values = parse_range(&part, 1, 31, None, true)?;
            values.extend(range_values);
            continue;
        }

        // Handle wildcard
        if part == "*" {
            for v in 1..=31 {
                values.insert(v);
            }
            continue;
        }

        // Handle single value
        let mut num: u32 = part.parse().map_err(|_| format!("Invalid day: {}", part))?;

        // Python compatibility: In 6/7-field format, day 0 is converted to day 1
        // LOWMAP in Python: ({}, {}, {0: 1}, {0: 1}, {7: 0}, {}, {})
        // Index 2 (DAY_FIELD) has {0: 1} mapping
        // This conversion is skipped for 5-field cron but applied for 6/7-field
        if num == 0 && allow_day_zero {
            num = 1;
        }

        if num < 1 || num > 31 {
            return Err(format!("Day {} out of range [1, 31]", num));
        }
        values.insert(num);
    }

    let mut result: Vec<u32> = values.into_iter().collect();
    result.sort();

    // If only L was specified, return empty days list
    if result.is_empty() && last_day {
        return Ok((vec![], true));
    }

    Ok((result, last_day))
}

/// Parse weekday field, handling names, nth weekday syntax, and lN (last weekday)
fn parse_weekday_field(field: &str, hash_id: Option<&str>) -> Result<Vec<u32>, String> {
    let mut values: HashSet<u32> = HashSet::new();

    for part in field.split(',') {
        let part = part.trim().to_lowercase();
        if part.is_empty() {
            continue;
        }

        // Handle ? placeholder (treat as *)
        if part == "?" {
            for v in 0..=6 {
                values.insert(v);
            }
            continue;
        }

        // Skip nth weekday syntax (handled separately in parse_nth_weekdays)
        if part.contains('#') {
            // Still need to include the weekday in the list
            let weekday_part = part.split('#').next().unwrap();
            let weekday = parse_weekday_value(weekday_part)?;
            values.insert(weekday);
            continue;
        }

        // Skip lN syntax (last weekday, handled separately in parse_nth_weekdays)
        if part.starts_with('l') && part.len() > 1 {
            // lN means last occurrence of weekday N
            let weekday_str = &part[1..];
            let weekday = parse_weekday_value(weekday_str)?;
            values.insert(weekday);
            continue;
        }

        // Handle hash expressions
        if part.starts_with('h') {
            let hash_values = parse_hash_field(&part, 0, 6, hash_id, 4)?;
            values.extend(hash_values);
            continue;
        }

        // Handle random expressions
        if part.starts_with('r') {
            let random_values = parse_random_field(&part, 0, 6)?;
            values.extend(random_values);
            continue;
        }

        // Handle step expressions
        if part.contains('/') {
            let step_values = parse_step(&part, 0, 6, Some(&DAY_NAMES), hash_id, false, 4)?;
            values.extend(step_values);
            continue;
        }

        // Handle range expressions
        if part.contains('-') {
            let range_values = parse_weekday_range(&part)?;
            values.extend(range_values);
            continue;
        }

        // Handle wildcard
        if part == "*" {
            for v in 0..=6 {
                values.insert(v);
            }
            continue;
        }

        // Reject NL patterns (e.g., 5L, 1L) - Python croniter only accepts LN format
        // Use NOT_ALPHA prefix to trigger CroniterNotAlphaError in lib.rs
        if part.len() > 1 && part.ends_with('l') {
            return Err(format!("NOT_ALPHA: Invalid weekday syntax '{}': use LN format (e.g., L5) instead of NL format", part));
        }

        // Handle single value
        let weekday = parse_weekday_value(&part)?;
        values.insert(weekday);
    }

    let mut result: Vec<u32> = values.into_iter().collect();
    result.sort();
    Ok(result)
}

/// Parse a single weekday value (name or number)
fn parse_weekday_value(value: &str) -> Result<u32, String> {
    let value = value.trim().to_lowercase();

    // Try name lookup
    for (name, num) in DAY_NAMES {
        if value == *name {
            return Ok(*num);
        }
    }

    // Try parsing as number
    let num: u32 = value.parse().map_err(|_| format!("Invalid weekday: {}", value))?;

    // Handle 7 as Sunday (some cron implementations)
    if num == 7 {
        return Ok(0);
    }

    if num > 6 {
        return Err(format!("Weekday {} out of range [0, 6]", num));
    }

    Ok(num)
}

/// Parse weekday range like sun-thu
fn parse_weekday_range(field: &str) -> Result<Vec<u32>, String> {
    let parts: Vec<&str> = field.split('-').collect();
    if parts.len() != 2 {
        return Err(format!("Invalid weekday range: {}", field));
    }

    let start = parse_weekday_value(parts[0])?;
    let end = parse_weekday_value(parts[1])?;

    if start == end {
        // Python behavior: X-X means the whole cycle (all weekdays)
        Ok((0..=6).collect())
    } else if start < end {
        Ok((start..=end).collect())
    } else {
        // Wrap around (e.g., fri-mon = 5,6,0,1)
        let mut values: Vec<u32> = (start..=6).collect();
        values.extend(0..=end);
        Ok(values)
    }
}

/// Parse nth weekday expressions like sat#1, wed#5, and lN (last weekday)
fn parse_nth_weekdays(field: &str) -> Result<Vec<(u32, u32)>, String> {
    let mut result = Vec::new();

    for part in field.split(',') {
        let part = part.trim().to_lowercase();

        // Reject NL patterns (e.g., 5L, 1L) - Python croniter only accepts LN format
        // Use NOT_ALPHA prefix to trigger CroniterNotAlphaError in lib.rs
        if part.len() > 1 && part.ends_with('l') && !part.starts_with('l') {
            return Err(format!("NOT_ALPHA: Invalid weekday syntax '{}': use LN format (e.g., L5) instead of NL format", part));
        }

        // Handle lN syntax (last occurrence of weekday N)
        if part.starts_with('l') && part.len() > 1 {
            let weekday_str = &part[1..];
            let weekday = parse_weekday_value(weekday_str)?;
            // Use 0 as a special marker for "last occurrence"
            result.push((weekday, 0));
            continue;
        }

        if !part.contains('#') {
            continue;
        }

        let parts: Vec<&str> = part.split('#').collect();
        if parts.len() != 2 {
            return Err(format!("Invalid nth weekday: {}", part));
        }

        let weekday = parse_weekday_value(parts[0])?;
        let nth: u32 = parts[1].parse().map_err(|_| format!("Invalid nth value: {}", parts[1]))?;

        if nth < 1 || nth > 5 {
            return Err(format!("Nth value {} out of range [1, 5]", nth));
        }

        result.push((weekday, nth));
    }

    Ok(result)
}

/// Check if a day field is an X-X range (like "5-5") which Python treats as '*'
fn is_x_x_range(field: &str) -> bool {
    let field = field.trim();
    if !field.contains('-') {
        return false;
    }
    // Don't match if it contains other operators
    if field.contains(',') || field.contains('/') {
        return false;
    }
    let parts: Vec<&str> = field.split('-').collect();
    if parts.len() != 2 {
        return false;
    }
    // Check if both parts are the same (numeric)
    let left = parts[0].trim();
    let right = parts[1].trim();
    left == right && left.parse::<u32>().is_ok()
}

/// Check if a weekday field is an X-X range (like "sun-sun" or "1-1") which Python treats as '*'
fn is_x_x_weekday_range(field: &str) -> bool {
    let field = field.trim().to_lowercase();
    if !field.contains('-') {
        return false;
    }
    // Don't match if it contains other operators or nth weekday syntax
    if field.contains(',') || field.contains('/') || field.contains('#') {
        return false;
    }
    let parts: Vec<&str> = field.split('-').collect();
    if parts.len() != 2 {
        return false;
    }
    let left = parts[0].trim();
    let right = parts[1].trim();

    // Check if both parts resolve to the same weekday value
    if let (Ok(left_val), Ok(right_val)) = (parse_weekday_value(left), parse_weekday_value(right)) {
        return left_val == right_val;
    }
    false
}

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

    #[test]
    fn test_parse_simple() {
        let expr = CronExpr::parse("*/5 * * * *", None, false).unwrap();
        assert_eq!(expr.minutes, vec![0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]);
    }

    #[test]
    fn test_parse_range() {
        let expr = CronExpr::parse("1-5 * * * *", None, false).unwrap();
        assert_eq!(expr.minutes, vec![1, 2, 3, 4, 5]);
    }

    #[test]
    fn test_parse_weekday_names() {
        let expr = CronExpr::parse("0 0 * * sun-thu", None, false).unwrap();
        assert_eq!(expr.weekdays, vec![0, 1, 2, 3, 4]);
    }

    #[test]
    fn test_keyword_expressions() {
        // @daily, @hourly, @weekly, @monthly, @yearly
        let expr = CronExpr::parse("@daily", None, false).unwrap();
        assert_eq!(expr.minutes, vec![0]);
        assert_eq!(expr.hours, vec![0]);

        let expr = CronExpr::parse("@hourly", None, false).unwrap();
        assert_eq!(expr.minutes, vec![0]);
    }

    #[test]
    fn test_hash_expressions() {
        let expr = CronExpr::parse("H * * * *", Some("test-id"), false).unwrap();
        assert_eq!(expr.minutes.len(), 1);

        let expr = CronExpr::parse("H(0-30) * * * *", Some("test-id"), false).unwrap();
        assert!(expr.minutes[0] <= 30);
    }

    #[test]
    fn test_step_expressions() {
        let expr = CronExpr::parse("*/15 * * * *", None, false).unwrap();
        assert_eq!(expr.minutes, vec![0, 15, 30, 45]);

        let expr = CronExpr::parse("1-10/2 * * * *", None, false).unwrap();
        assert_eq!(expr.minutes, vec![1, 3, 5, 7, 9]);
    }

    #[test]
    fn test_last_day() {
        let expr = CronExpr::parse("0 0 L * *", None, false).unwrap();
        assert!(expr.last_day);
    }

    #[test]
    fn test_nth_weekday() {
        let expr = CronExpr::parse("0 0 * * sat#1", None, false).unwrap();
        assert!(!expr.nth_weekdays.is_empty());
    }

    #[test]
    fn test_question_mark() {
        let expr = CronExpr::parse("0 0 ? * *", None, false).unwrap();
        assert_eq!(expr.days.len(), 31);
    }

    #[test]
    fn test_six_field_with_seconds() {
        let expr = CronExpr::parse("30 0 * * * *", None, true).unwrap();
        assert_eq!(expr.seconds, vec![30]);
        assert!(expr.has_seconds);
    }

    #[test]
    fn test_invalid_expressions() {
        assert!(CronExpr::parse("invalid", None, false).is_err());
        assert!(CronExpr::parse("60 * * * *", None, false).is_err());
        assert!(CronExpr::parse("* 25 * * *", None, false).is_err());
    }

    #[test]
    fn test_l_minus_n_rejection() {
        // Test that L-N patterns are rejected in day field (Python croniter behavior)
        assert!(CronExpr::parse("0 0 L-1 * *", None, false).is_err(), "L-1 should be rejected");
        assert!(CronExpr::parse("0 0 L-7 * *", None, false).is_err(), "L-7 should be rejected");

        // Test that N-L patterns are accepted
        assert!(CronExpr::parse("0 0 25-L * *", None, false).is_ok(), "25-L should be accepted");
    }

    #[test]
    fn test_nl_rejection_weekday() {
        // Test that NL patterns are rejected in weekday field (Python croniter behavior)
        assert!(CronExpr::parse("0 0 * * 5L", None, false).is_err(), "5L should be rejected");
        assert!(CronExpr::parse("0 0 * * 1L", None, false).is_err(), "1L should be rejected");

        // Test that LN patterns are accepted
        assert!(CronExpr::parse("0 0 * * L5", None, false).is_ok(), "L5 should be accepted");
        assert!(CronExpr::parse("0 0 * * L1", None, false).is_ok(), "L1 should be accepted");
    }
}
