//! Date arithmetic: today + 3 weeks, date ranges, days until X, named dates.

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

/// User preference for ambiguous date formats like 3/4/2025.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum DateFormat {
    /// MM/DD/YYYY (US default)
    #[default]
    US,
    /// DD/MM/YYYY (European)
    EU,
}

/// A compound calendar duration (years, months, weeks, days).
/// Stored as signed integers so negation is straightforward.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CalendarDuration {
    pub years: i64,
    pub months: i64,
    pub weeks: i64,
    pub days: i64,
}

impl CalendarDuration {
    pub fn zero() -> Self {
        Self {
            years: 0,
            months: 0,
            weeks: 0,
            days: 0,
        }
    }

    /// Approximate total days (30-day months, 365-day years).
    pub fn total_days_approx(&self) -> i64 {
        self.years * 365 + self.months * 30 + self.weeks * 7 + self.days
    }

    /// Return the negated duration.
    pub fn negate(&self) -> Self {
        Self {
            years: -self.years,
            months: -self.months,
            weeks: -self.weeks,
            days: -self.days,
        }
    }
}

impl std::fmt::Display for CalendarDuration {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut parts = Vec::new();
        if self.years != 0 {
            let label = if self.years.abs() == 1 { "year" } else { "years" };
            parts.push(format!("{} {}", self.years.abs(), label));
        }
        if self.months != 0 {
            let label = if self.months.abs() == 1 {
                "month"
            } else {
                "months"
            };
            parts.push(format!("{} {}", self.months.abs(), label));
        }
        if self.weeks != 0 {
            let label = if self.weeks.abs() == 1 { "week" } else { "weeks" };
            parts.push(format!("{} {}", self.weeks.abs(), label));
        }
        if self.days != 0 {
            let label = if self.days.abs() == 1 { "day" } else { "days" };
            parts.push(format!("{} {}", self.days.abs(), label));
        }
        if parts.is_empty() {
            write!(f, "0 days")
        } else {
            write!(f, "{}", parts.join(" "))
        }
    }
}

/// Add a `CalendarDuration` to a date. Months/years use `chrono::Months` for
/// correct calendar arithmetic; weeks and days are added as simple day offsets.
pub fn add_duration(date: NaiveDate, dur: &CalendarDuration) -> Option<NaiveDate> {
    let mut result = date;

    // Years (converted to months for chrono)
    if dur.years != 0 {
        if dur.years > 0 {
            result = result.checked_add_months(Months::new((dur.years * 12) as u32))?;
        } else {
            result =
                result.checked_sub_months(Months::new((dur.years.abs() * 12) as u32))?;
        }
    }

    // Months
    if dur.months != 0 {
        if dur.months > 0 {
            result = result.checked_add_months(Months::new(dur.months as u32))?;
        } else {
            result =
                result.checked_sub_months(Months::new(dur.months.unsigned_abs() as u32))?;
        }
    }

    // Weeks + days
    let total_days = dur.weeks * 7 + dur.days;
    if total_days != 0 {
        result = result.checked_add_signed(chrono::Duration::days(total_days))?;
    }

    Some(result)
}

/// Subtract a `CalendarDuration` from a date.
pub fn sub_duration(date: NaiveDate, dur: &CalendarDuration) -> Option<NaiveDate> {
    add_duration(date, &dur.negate())
}

/// Compute the signed difference in whole days: `to - from`.
pub fn days_between(from: NaiveDate, to: NaiveDate) -> i64 {
    (to - from).num_days()
}

/// Resolve a named date to the next occurrence on or after `reference`.
/// Returns `None` for unrecognized names.
pub fn resolve_named_date(name: &str, reference: NaiveDate) -> Option<NaiveDate> {
    let lower = name.to_lowercase();
    let (month, day) = match lower.as_str() {
        "christmas" | "xmas" => (12, 25),
        "newyear" | "newyears" | "new year" | "new years" | "new year's" => (1, 1),
        "valentines" | "valentine's" | "valentines day" => (2, 14),
        "halloween" => (10, 31),
        "independence day" | "july 4th" | "fourth of july" => (7, 4),
        _ => return None,
    };

    let this_year = NaiveDate::from_ymd_opt(reference.year(), month, day)?;
    if reference <= this_year {
        Some(this_year)
    } else {
        NaiveDate::from_ymd_opt(reference.year() + 1, month, day)
    }
}

/// Parse a month name (full or abbreviated) to its 1-based number.
pub fn month_number(name: &str) -> Option<u32> {
    match name.to_lowercase().as_str() {
        "january" | "jan" => Some(1),
        "february" | "feb" => Some(2),
        "march" | "mar" => Some(3),
        "april" | "apr" => Some(4),
        "may" => Some(5),
        "june" | "jun" => Some(6),
        "july" | "jul" => Some(7),
        "august" | "aug" => Some(8),
        "september" | "sep" | "sept" => Some(9),
        "october" | "oct" => Some(10),
        "november" | "nov" => Some(11),
        "december" | "dec" => Some(12),
        _ => None,
    }
}

/// Format a date for display according to the given format preference.
pub fn format_date(date: NaiveDate, format: DateFormat) -> String {
    match format {
        DateFormat::US => date.format("%B %-d, %Y").to_string(),
        DateFormat::EU => date.format("%-d %B %Y").to_string(),
    }
}

/// Format a day-count delta for display, with an optional month breakdown.
pub fn format_day_delta(days: i64) -> String {
    let abs_days = days.unsigned_abs();
    if abs_days == 0 {
        return "0 days".to_string();
    }
    if abs_days >= 30 {
        let months = abs_days / 30;
        let remaining = abs_days % 30;
        let m_label = if months == 1 { "month" } else { "months" };
        if remaining > 0 {
            let d_label = if remaining == 1 { "day" } else { "days" };
            format!(
                "{} days ({} {} {} {})",
                abs_days, months, m_label, remaining, d_label
            )
        } else {
            format!("{} days ({} {})", abs_days, months, m_label)
        }
    } else {
        let d_label = if abs_days == 1 { "day" } else { "days" };
        format!("{} {}", abs_days, d_label)
    }
}

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

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

    // -- CalendarDuration --

    #[test]
    fn test_duration_display_mixed() {
        let dur = CalendarDuration {
            years: 1,
            months: 2,
            weeks: 3,
            days: 4,
        };
        assert_eq!(dur.to_string(), "1 year 2 months 3 weeks 4 days");
    }

    #[test]
    fn test_duration_display_zero() {
        assert_eq!(CalendarDuration::zero().to_string(), "0 days");
    }

    #[test]
    fn test_duration_total_days_approx() {
        let dur = CalendarDuration {
            years: 1,
            months: 0,
            weeks: 0,
            days: 0,
        };
        assert_eq!(dur.total_days_approx(), 365);
    }

    // -- add_duration / sub_duration --

    #[test]
    fn test_add_weeks_and_days() {
        // March 17 + 3 weeks 2 days = April 9
        let result = add_duration(
            d(2026, 3, 17),
            &CalendarDuration {
                years: 0,
                months: 0,
                weeks: 3,
                days: 2,
            },
        );
        assert_eq!(result, Some(d(2026, 4, 9)));
    }

    #[test]
    fn test_add_one_year() {
        let result = add_duration(
            d(2026, 3, 17),
            &CalendarDuration {
                years: 1,
                months: 0,
                weeks: 0,
                days: 0,
            },
        );
        assert_eq!(result, Some(d(2027, 3, 17)));
    }

    #[test]
    fn test_add_one_month_end_of_month_clamp() {
        // Jan 31 + 1 month = Feb 28 (non-leap)
        let result = add_duration(
            d(2026, 1, 31),
            &CalendarDuration {
                years: 0,
                months: 1,
                weeks: 0,
                days: 0,
            },
        );
        assert_eq!(result, Some(d(2026, 2, 28)));
    }

    #[test]
    fn test_sub_30_days() {
        // Jan 15, 2025 - 30 days = Dec 16, 2024
        let result = sub_duration(
            d(2025, 1, 15),
            &CalendarDuration {
                years: 0,
                months: 0,
                weeks: 0,
                days: 30,
            },
        );
        assert_eq!(result, Some(d(2024, 12, 16)));
    }

    #[test]
    fn test_leap_year_add_one_day() {
        let result = add_duration(
            d(2024, 2, 28),
            &CalendarDuration {
                years: 0,
                months: 0,
                weeks: 0,
                days: 1,
            },
        );
        assert_eq!(result, Some(d(2024, 2, 29)));
    }

    #[test]
    fn test_year_boundary() {
        let result = add_duration(
            d(2025, 12, 30),
            &CalendarDuration {
                years: 0,
                months: 0,
                weeks: 0,
                days: 5,
            },
        );
        assert_eq!(result, Some(d(2026, 1, 4)));
    }

    // -- days_between --

    #[test]
    fn test_days_between_same() {
        assert_eq!(days_between(d(2026, 3, 17), d(2026, 3, 17)), 0);
    }

    #[test]
    fn test_days_between_positive() {
        // March 12 to July 30 = 140 days
        assert_eq!(days_between(d(2026, 3, 12), d(2026, 7, 30)), 140);
    }

    #[test]
    fn test_days_between_negative() {
        assert_eq!(days_between(d(2026, 3, 17), d(2026, 3, 1)), -16);
    }

    // -- resolve_named_date --

    #[test]
    fn test_christmas_before() {
        assert_eq!(
            resolve_named_date("Christmas", d(2026, 3, 17)),
            Some(d(2026, 12, 25))
        );
    }

    #[test]
    fn test_christmas_after() {
        assert_eq!(
            resolve_named_date("Christmas", d(2026, 12, 26)),
            Some(d(2027, 12, 25))
        );
    }

    #[test]
    fn test_new_years() {
        assert_eq!(
            resolve_named_date("newyear", d(2026, 3, 17)),
            Some(d(2027, 1, 1))
        );
    }

    #[test]
    fn test_halloween() {
        assert_eq!(
            resolve_named_date("halloween", d(2026, 3, 17)),
            Some(d(2026, 10, 31))
        );
    }

    #[test]
    fn test_unknown_named_date() {
        assert_eq!(resolve_named_date("festivus", d(2026, 3, 17)), None);
    }

    // -- month_number --

    #[test]
    fn test_month_full_name() {
        assert_eq!(month_number("January"), Some(1));
        assert_eq!(month_number("december"), Some(12));
    }

    #[test]
    fn test_month_abbreviation() {
        assert_eq!(month_number("jan"), Some(1));
        assert_eq!(month_number("Sep"), Some(9));
    }

    // -- format_date --

    #[test]
    fn test_format_date_us() {
        assert_eq!(format_date(d(2025, 1, 15), DateFormat::US), "January 15, 2025");
    }

    #[test]
    fn test_format_date_eu() {
        assert_eq!(format_date(d(2025, 1, 15), DateFormat::EU), "15 January 2025");
    }

    // -- format_day_delta --

    #[test]
    fn test_format_delta_zero() {
        assert_eq!(format_day_delta(0), "0 days");
    }

    #[test]
    fn test_format_delta_one_day() {
        assert_eq!(format_day_delta(1), "1 day");
    }

    #[test]
    fn test_format_delta_short() {
        assert_eq!(format_day_delta(16), "16 days");
    }

    #[test]
    fn test_format_delta_with_months() {
        assert_eq!(format_day_delta(140), "140 days (4 months 20 days)");
    }

    #[test]
    fn test_format_delta_exact_months() {
        assert_eq!(format_day_delta(60), "60 days (2 months)");
    }
}
