//! Business day calculations: skip weekends, configurable holiday calendars,
//! forward and backward counting.

use chrono::{Datelike, NaiveDate, Weekday};

/// Configuration for business day calculations.
#[derive(Debug, Clone)]
pub struct BusinessDayConfig {
    /// Specific dates to treat as holidays (non-business days).
    /// An empty list means only weekends are skipped.
    pub holidays: Vec<NaiveDate>,
}

impl Default for BusinessDayConfig {
    fn default() -> Self {
        Self {
            holidays: Vec::new(),
        }
    }
}

impl BusinessDayConfig {
    /// Check whether a given date is a business day.
    pub fn is_business_day(&self, date: NaiveDate) -> bool {
        let wd = date.weekday();
        if wd == Weekday::Sat || wd == Weekday::Sun {
            return false;
        }
        !self.holidays.contains(&date)
    }
}

/// Add `count` business days to `start`, skipping weekends and holidays.
/// Counting begins on the day **after** `start`.
pub fn add_business_days(
    start: NaiveDate,
    count: i64,
    config: &BusinessDayConfig,
) -> Option<NaiveDate> {
    if count == 0 {
        return Some(start);
    }

    let mut remaining = count;
    let mut current = start;

    while remaining > 0 {
        current = current.checked_add_signed(chrono::Duration::days(1))?;
        if config.is_business_day(current) {
            remaining -= 1;
        }
    }

    Some(current)
}

/// Subtract `count` business days from `start` (go backward).
/// Counting begins on the day **before** `start`.
pub fn sub_business_days(
    start: NaiveDate,
    count: i64,
    config: &BusinessDayConfig,
) -> Option<NaiveDate> {
    if count == 0 {
        return Some(start);
    }

    let mut remaining = count;
    let mut current = start;

    while remaining > 0 {
        current = current.checked_sub_signed(chrono::Duration::days(1))?;
        if config.is_business_day(current) {
            remaining -= 1;
        }
    }

    Some(current)
}

/// Count the number of business days between two dates (exclusive of endpoints,
/// or inclusive depending on convention -- here we count days strictly between
/// `from` and `to`, not including `from` but including `to`).
pub fn business_days_between(
    from: NaiveDate,
    to: NaiveDate,
    config: &BusinessDayConfig,
) -> i64 {
    if from >= to {
        return 0;
    }
    let mut count = 0i64;
    let mut current = from;
    while current < to {
        current += chrono::Duration::days(1);
        if config.is_business_day(current) {
            count += 1;
        }
    }
    count
}

// ---------------------------------------------------------------------------
// US Federal Holiday Calendar
// ---------------------------------------------------------------------------

/// Generate US federal holiday dates for a given year.
///
/// Includes: New Year's Day, MLK Day, Presidents' Day, Memorial Day,
/// Independence Day, Labor Day, Columbus Day, Veterans Day, Thanksgiving, Christmas.
pub fn us_holidays(year: i32) -> Vec<NaiveDate> {
    let mut holidays = Vec::new();

    // New Year's Day
    if let Some(d) = NaiveDate::from_ymd_opt(year, 1, 1) {
        holidays.push(d);
    }
    // MLK Day -- 3rd Monday of January
    if let Some(d) = nth_weekday_of_month(year, 1, Weekday::Mon, 3) {
        holidays.push(d);
    }
    // Presidents' Day -- 3rd Monday of February
    if let Some(d) = nth_weekday_of_month(year, 2, Weekday::Mon, 3) {
        holidays.push(d);
    }
    // Memorial Day -- last Monday of May
    if let Some(d) = last_weekday_of_month(year, 5, Weekday::Mon) {
        holidays.push(d);
    }
    // Independence Day
    if let Some(d) = NaiveDate::from_ymd_opt(year, 7, 4) {
        holidays.push(d);
    }
    // Labor Day -- 1st Monday of September
    if let Some(d) = nth_weekday_of_month(year, 9, Weekday::Mon, 1) {
        holidays.push(d);
    }
    // Columbus Day -- 2nd Monday of October
    if let Some(d) = nth_weekday_of_month(year, 10, Weekday::Mon, 2) {
        holidays.push(d);
    }
    // Veterans Day
    if let Some(d) = NaiveDate::from_ymd_opt(year, 11, 11) {
        holidays.push(d);
    }
    // Thanksgiving -- 4th Thursday of November
    if let Some(d) = nth_weekday_of_month(year, 11, Weekday::Thu, 4) {
        holidays.push(d);
    }
    // Christmas Day
    if let Some(d) = NaiveDate::from_ymd_opt(year, 12, 25) {
        holidays.push(d);
    }

    holidays
}

/// Find the Nth occurrence of a weekday in a given month (1-indexed).
pub fn nth_weekday_of_month(
    year: i32,
    month: u32,
    weekday: Weekday,
    n: u32,
) -> Option<NaiveDate> {
    let first = NaiveDate::from_ymd_opt(year, month, 1)?;
    let first_wd = first.weekday();
    let days_until = (weekday.num_days_from_monday() as i32
        - first_wd.num_days_from_monday() as i32
        + 7)
        % 7;
    let day = 1 + days_until as u32 + (n - 1) * 7;
    NaiveDate::from_ymd_opt(year, month, day)
}

/// Find the last occurrence of a weekday in a given month.
pub fn last_weekday_of_month(year: i32, month: u32, weekday: Weekday) -> Option<NaiveDate> {
    let next_month = if month == 12 {
        NaiveDate::from_ymd_opt(year + 1, 1, 1)?
    } else {
        NaiveDate::from_ymd_opt(year, month + 1, 1)?
    };
    let last_day = next_month.pred_opt()?;
    let last_wd = last_day.weekday();
    let days_back = (last_wd.num_days_from_monday() as i32
        - weekday.num_days_from_monday() as i32
        + 7)
        % 7;
    let day = last_day.day() - days_back as u32;
    NaiveDate::from_ymd_opt(year, month, day)
}

/// Resolve the next occurrence of a named weekday after `reference`.
/// If `reference` IS that day, returns the **following** week's occurrence.
pub fn next_weekday(name: &str, reference: NaiveDate) -> Option<NaiveDate> {
    let target = parse_weekday(name)?;
    let today_wd = reference.weekday();
    let days_ahead = (target.num_days_from_monday() as i32
        - today_wd.num_days_from_monday() as i32
        + 7)
        % 7;
    let days_ahead = if days_ahead == 0 { 7 } else { days_ahead };
    reference.checked_add_signed(chrono::Duration::days(days_ahead as i64))
}

/// Parse a weekday name (full or abbreviated, case-insensitive).
pub fn parse_weekday(name: &str) -> Option<Weekday> {
    match name.to_lowercase().as_str() {
        "monday" | "mon" => Some(Weekday::Mon),
        "tuesday" | "tue" | "tues" => Some(Weekday::Tue),
        "wednesday" | "wed" => Some(Weekday::Wed),
        "thursday" | "thu" | "thur" | "thurs" => Some(Weekday::Thu),
        "friday" | "fri" => Some(Weekday::Fri),
        "saturday" | "sat" => Some(Weekday::Sat),
        "sunday" | "sun" => Some(Weekday::Sun),
        _ => None,
    }
}

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

    fn d(y: i32, m: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(y, m, day).unwrap()
    }

    // -- is_business_day --

    #[test]
    fn test_weekday_is_business_day() {
        let cfg = BusinessDayConfig::default();
        assert!(cfg.is_business_day(d(2026, 3, 17))); // Tuesday
    }

    #[test]
    fn test_weekend_not_business_day() {
        let cfg = BusinessDayConfig::default();
        assert!(!cfg.is_business_day(d(2026, 3, 21))); // Saturday
        assert!(!cfg.is_business_day(d(2026, 3, 22))); // Sunday
    }

    #[test]
    fn test_holiday_not_business_day() {
        let cfg = BusinessDayConfig {
            holidays: vec![d(2025, 12, 25)],
        };
        assert!(!cfg.is_business_day(d(2025, 12, 25))); // Thursday Christmas
    }

    // -- add_business_days --

    #[test]
    fn test_add_10_from_monday() {
        // March 16 2026 is Monday. 10 business days forward:
        // Day 1-5: Tue 17 ... Mon 23 (skipping Sat/Sun 21-22)
        // Wait: Day1=Tue17, Day2=Wed18, Day3=Thu19, Day4=Fri20,
        //  skip Sat21 Sun22,
        //  Day5=Mon23, Day6=Tue24, Day7=Wed25, Day8=Thu26, Day9=Fri27,
        //  skip Sat28 Sun29,
        //  Day10=Mon30
        let cfg = BusinessDayConfig::default();
        assert_eq!(add_business_days(d(2026, 3, 16), 10, &cfg), Some(d(2026, 3, 30)));
    }

    #[test]
    fn test_add_10_from_wednesday() {
        // March 18 2026 is Wednesday.
        let cfg = BusinessDayConfig::default();
        assert_eq!(
            add_business_days(d(2026, 3, 18), 10, &cfg),
            Some(d(2026, 4, 1))
        );
    }

    #[test]
    fn test_add_zero() {
        let cfg = BusinessDayConfig::default();
        assert_eq!(add_business_days(d(2026, 3, 18), 0, &cfg), Some(d(2026, 3, 18)));
    }

    #[test]
    fn test_add_from_saturday() {
        // Saturday March 21: 1 biz day → skip Sun → Mon 23
        let cfg = BusinessDayConfig::default();
        assert_eq!(add_business_days(d(2026, 3, 21), 1, &cfg), Some(d(2026, 3, 23)));
    }

    #[test]
    fn test_add_with_holiday() {
        // Dec 23 2025 (Tue) + 3 biz days, Christmas Dec 25 is holiday
        // Day1=Wed24, skip Thu25 (holiday), Day2=Fri26, skip Sat27 Sun28, Day3=Mon29
        let cfg = BusinessDayConfig {
            holidays: vec![d(2025, 12, 25)],
        };
        assert_eq!(
            add_business_days(d(2025, 12, 23), 3, &cfg),
            Some(d(2025, 12, 29))
        );
    }

    #[test]
    fn test_add_no_holidays_christmas_counts() {
        // Same scenario but no holiday calendar
        let cfg = BusinessDayConfig::default();
        assert_eq!(
            add_business_days(d(2025, 12, 23), 3, &cfg),
            Some(d(2025, 12, 26))
        );
    }

    // -- sub_business_days --

    #[test]
    fn test_sub_5_from_wednesday() {
        // March 18 Wed: Day1=Tue17, Day2=Mon16, skip Sun15 Sat14, Day3=Fri13, Day4=Thu12, Day5=Wed11
        let cfg = BusinessDayConfig::default();
        assert_eq!(
            sub_business_days(d(2026, 3, 18), 5, &cfg),
            Some(d(2026, 3, 11))
        );
    }

    #[test]
    fn test_sub_5_from_monday() {
        // March 16 Mon: Day1=Fri13, skip Sat14 Sun15 (already behind),
        // wait: Mon16 back 1 cal day = Sun15 (skip), Sat14 (skip), Fri13 (day1),
        //       Thu12 (day2), Wed11 (day3), Tue10 (day4), Mon9 (day5)
        let cfg = BusinessDayConfig::default();
        assert_eq!(
            sub_business_days(d(2026, 3, 16), 5, &cfg),
            Some(d(2026, 3, 9))
        );
    }

    // -- business_days_between --

    #[test]
    fn test_between_same_day() {
        let cfg = BusinessDayConfig::default();
        assert_eq!(business_days_between(d(2026, 3, 17), d(2026, 3, 17), &cfg), 0);
    }

    #[test]
    fn test_between_one_week() {
        // Mon to next Mon = 5 business days
        let cfg = BusinessDayConfig::default();
        assert_eq!(
            business_days_between(d(2026, 3, 16), d(2026, 3, 23), &cfg),
            5
        );
    }

    // -- us_holidays --

    #[test]
    fn test_us_holidays_2025() {
        let holidays = us_holidays(2025);
        assert!(holidays.contains(&d(2025, 1, 1)));      // New Year's
        assert!(holidays.contains(&d(2025, 7, 4)));      // Independence Day
        assert!(holidays.contains(&d(2025, 12, 25)));    // Christmas
        assert!(holidays.contains(&d(2025, 1, 20)));     // MLK Day
        assert!(holidays.contains(&d(2025, 11, 27)));    // Thanksgiving
        assert!(holidays.contains(&d(2025, 5, 26)));     // Memorial Day
        assert!(holidays.contains(&d(2025, 9, 1)));      // Labor Day
    }

    // -- next_weekday --

    #[test]
    fn test_next_friday_from_tuesday() {
        // March 17 2026 = Tuesday, next Friday = March 20
        assert_eq!(next_weekday("Friday", d(2026, 3, 17)), Some(d(2026, 3, 20)));
    }

    #[test]
    fn test_next_tuesday_from_tuesday() {
        // Same day goes to next week
        assert_eq!(next_weekday("Tuesday", d(2026, 3, 17)), Some(d(2026, 3, 24)));
    }

    // -- parse_weekday --

    #[test]
    fn test_parse_weekday() {
        assert_eq!(parse_weekday("Monday"), Some(Weekday::Mon));
        assert_eq!(parse_weekday("fri"), Some(Weekday::Fri));
        assert_eq!(parse_weekday("SUNDAY"), Some(Weekday::Sun));
        assert_eq!(parse_weekday("blurday"), None);
    }
}
