//! Relative time expressions: "2 hours ago", "in 3 days", "next Wednesday",
//! "last Friday", "tomorrow at 3pm".

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

/// The direction of a relative expression.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelativeDirection {
    /// In the past: "2 hours ago", "last Monday"
    Past,
    /// In the future: "in 3 days", "next Friday"
    Future,
}

/// A time unit for relative offsets.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelativeUnit {
    Minutes,
    Hours,
    Days,
    Weeks,
    Months,
}

/// The result of evaluating a relative time expression.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelativeResult {
    pub date: NaiveDate,
    /// Optional time-of-day (present for time-level expressions or "tomorrow at 3pm").
    pub time: Option<NaiveTime>,
}

/// Evaluate a relative offset: "3 days ago", "in 2 weeks", etc.
///
/// For sub-day units (Hours, Minutes), the `now_time` is used as the base
/// and the result will include a time component. For day-and-above units
/// only the date is adjusted.
pub fn eval_relative_offset(
    amount: i64,
    unit: RelativeUnit,
    direction: RelativeDirection,
    now_date: NaiveDate,
    now_time: NaiveTime,
) -> Option<RelativeResult> {
    let signed = match direction {
        RelativeDirection::Past => -amount,
        RelativeDirection::Future => amount,
    };

    match unit {
        RelativeUnit::Minutes => {
            let total_minutes =
                now_time.hour() as i64 * 60 + now_time.minute() as i64 + signed;
            let day_offset = total_minutes.div_euclid(24 * 60);
            let normalized = total_minutes.rem_euclid(24 * 60);
            let hour = (normalized / 60) as u32;
            let minute = (normalized % 60) as u32;
            let date = now_date.checked_add_signed(Duration::days(day_offset))?;
            Some(RelativeResult {
                date,
                time: NaiveTime::from_hms_opt(hour, minute, 0),
            })
        }
        RelativeUnit::Hours => {
            let total_minutes =
                now_time.hour() as i64 * 60 + now_time.minute() as i64 + signed * 60;
            let day_offset = total_minutes.div_euclid(24 * 60);
            let normalized = total_minutes.rem_euclid(24 * 60);
            let hour = (normalized / 60) as u32;
            let minute = (normalized % 60) as u32;
            let date = now_date.checked_add_signed(Duration::days(day_offset))?;
            Some(RelativeResult {
                date,
                time: NaiveTime::from_hms_opt(hour, minute, 0),
            })
        }
        RelativeUnit::Days => {
            let date = now_date.checked_add_signed(Duration::days(signed))?;
            Some(RelativeResult { date, time: None })
        }
        RelativeUnit::Weeks => {
            let date = now_date.checked_add_signed(Duration::weeks(signed))?;
            Some(RelativeResult { date, time: None })
        }
        RelativeUnit::Months => {
            use chrono::Months;
            let date = if signed > 0 {
                now_date.checked_add_months(Months::new(signed as u32))?
            } else {
                now_date.checked_sub_months(Months::new((-signed) as u32))?
            };
            Some(RelativeResult { date, time: None })
        }
    }
}

/// Evaluate "next <weekday>" or "last <weekday>".
///
/// - `next`: finds the **next** occurrence of the weekday strictly after `reference`.
/// - `last`: finds the most recent **past** occurrence strictly before `reference`.
///
/// Optionally takes a time-of-day (e.g. "next Wednesday at 3pm").
pub fn eval_day_of_week_ref(
    weekday: Weekday,
    direction: RelativeDirection,
    reference: NaiveDate,
    time_of_day: Option<NaiveTime>,
) -> Option<RelativeResult> {
    let current_wd = reference.weekday();
    let date = match direction {
        RelativeDirection::Future => {
            let days_ahead = (weekday.num_days_from_monday() as i32
                - current_wd.num_days_from_monday() as i32
                + 7)
                % 7;
            let days_ahead = if days_ahead == 0 { 7 } else { days_ahead };
            reference.checked_add_signed(Duration::days(days_ahead as i64))?
        }
        RelativeDirection::Past => {
            let days_back = (current_wd.num_days_from_monday() as i32
                - weekday.num_days_from_monday() as i32
                + 7)
                % 7;
            let days_back = if days_back == 0 { 7 } else { days_back };
            reference.checked_sub_signed(Duration::days(days_back as i64))?
        }
    };

    Some(RelativeResult {
        date,
        time: time_of_day,
    })
}

/// Evaluate "tomorrow" / "yesterday" with an optional time-of-day.
pub fn eval_named_relative_day(
    offset_days: i64,
    reference: NaiveDate,
    time_of_day: Option<NaiveTime>,
) -> Option<RelativeResult> {
    let date = reference.checked_add_signed(Duration::days(offset_days))?;
    Some(RelativeResult {
        date,
        time: time_of_day,
    })
}

/// Format a `RelativeResult` for display.
pub fn format_relative_result(
    result: &RelativeResult,
    date_format: crate::datetime::date_math::DateFormat,
) -> String {
    let date_str = result
        .date
        .format("%A, %B %-d, %Y")
        .to_string();
    match &result.time {
        Some(t) => {
            let (h12, is_pm) = crate::datetime::time_math::to_12h(t.hour());
            let ampm = if is_pm { "PM" } else { "AM" };
            format!("{} at {}:{:02} {}", date_str, h12, t.minute(), ampm)
        }
        None => match date_format {
            crate::datetime::date_math::DateFormat::US => {
                crate::datetime::date_math::format_date(result.date, date_format)
            }
            crate::datetime::date_math::DateFormat::EU => {
                crate::datetime::date_math::format_date(result.date, date_format)
            }
        },
    }
}

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

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

    fn t(h: u32, m: u32) -> NaiveTime {
        NaiveTime::from_hms_opt(h, m, 0).unwrap()
    }

    // -- eval_relative_offset --

    #[test]
    fn test_3_days_ago() {
        let r = eval_relative_offset(3, RelativeUnit::Days, RelativeDirection::Past, d(2026, 3, 17), t(10, 0)).unwrap();
        assert_eq!(r.date, d(2026, 3, 14));
        assert_eq!(r.time, None);
    }

    #[test]
    fn test_in_2_weeks() {
        let r = eval_relative_offset(2, RelativeUnit::Weeks, RelativeDirection::Future, d(2026, 3, 17), t(10, 0)).unwrap();
        assert_eq!(r.date, d(2026, 3, 31));
        assert_eq!(r.time, None);
    }

    #[test]
    fn test_2_hours_ago() {
        let r = eval_relative_offset(2, RelativeUnit::Hours, RelativeDirection::Past, d(2026, 3, 17), t(10, 30)).unwrap();
        assert_eq!(r.date, d(2026, 3, 17));
        assert_eq!(r.time, Some(t(8, 30)));
    }

    #[test]
    fn test_5_hours_from_now_crossing_midnight() {
        // 10:00 PM + 5 hours = 3:00 AM next day
        let r = eval_relative_offset(5, RelativeUnit::Hours, RelativeDirection::Future, d(2026, 3, 17), t(22, 0)).unwrap();
        assert_eq!(r.date, d(2026, 3, 18));
        assert_eq!(r.time, Some(t(3, 0)));
    }

    #[test]
    fn test_45_minutes_ago() {
        let r = eval_relative_offset(45, RelativeUnit::Minutes, RelativeDirection::Past, d(2026, 3, 17), t(10, 30)).unwrap();
        assert_eq!(r.date, d(2026, 3, 17));
        assert_eq!(r.time, Some(t(9, 45)));
    }

    #[test]
    fn test_in_3_months() {
        let r = eval_relative_offset(3, RelativeUnit::Months, RelativeDirection::Future, d(2026, 3, 17), t(10, 0)).unwrap();
        assert_eq!(r.date, d(2026, 6, 17));
        assert_eq!(r.time, None);
    }

    #[test]
    fn test_2_months_ago() {
        let r = eval_relative_offset(2, RelativeUnit::Months, RelativeDirection::Past, d(2026, 3, 17), t(10, 0)).unwrap();
        assert_eq!(r.date, d(2026, 1, 17));
    }

    // -- eval_day_of_week_ref --

    #[test]
    fn test_next_wednesday() {
        // March 17 2026 is Tuesday, next Wednesday = March 18
        let r = eval_day_of_week_ref(Weekday::Wed, RelativeDirection::Future, d(2026, 3, 17), None).unwrap();
        assert_eq!(r.date, d(2026, 3, 18));
    }

    #[test]
    fn test_next_tuesday_from_tuesday() {
        // Same day → next week
        let r = eval_day_of_week_ref(Weekday::Tue, RelativeDirection::Future, d(2026, 3, 17), None).unwrap();
        assert_eq!(r.date, d(2026, 3, 24));
    }

    #[test]
    fn test_last_monday() {
        // March 17 Tue, last Monday = March 16
        let r = eval_day_of_week_ref(Weekday::Mon, RelativeDirection::Past, d(2026, 3, 17), None).unwrap();
        assert_eq!(r.date, d(2026, 3, 16));
    }

    #[test]
    fn test_last_tuesday_from_tuesday() {
        // Same day → previous week
        let r = eval_day_of_week_ref(Weekday::Tue, RelativeDirection::Past, d(2026, 3, 17), None).unwrap();
        assert_eq!(r.date, d(2026, 3, 10));
    }

    #[test]
    fn test_next_friday_at_3pm() {
        let r = eval_day_of_week_ref(Weekday::Fri, RelativeDirection::Future, d(2026, 3, 17), Some(t(15, 0))).unwrap();
        assert_eq!(r.date, d(2026, 3, 20));
        assert_eq!(r.time, Some(t(15, 0)));
    }

    // -- eval_named_relative_day --

    #[test]
    fn test_tomorrow() {
        let r = eval_named_relative_day(1, d(2026, 3, 17), None).unwrap();
        assert_eq!(r.date, d(2026, 3, 18));
    }

    #[test]
    fn test_yesterday() {
        let r = eval_named_relative_day(-1, d(2026, 3, 17), None).unwrap();
        assert_eq!(r.date, d(2026, 3, 16));
    }

    #[test]
    fn test_tomorrow_at_3pm() {
        let r = eval_named_relative_day(1, d(2026, 3, 17), Some(t(15, 0))).unwrap();
        assert_eq!(r.date, d(2026, 3, 18));
        assert_eq!(r.time, Some(t(15, 0)));
    }

    // -- format_relative_result --

    #[test]
    fn test_format_date_only() {
        let r = RelativeResult {
            date: d(2026, 3, 20),
            time: None,
        };
        assert_eq!(
            format_relative_result(&r, crate::datetime::date_math::DateFormat::US),
            "March 20, 2026"
        );
    }

    #[test]
    fn test_format_with_time() {
        let r = RelativeResult {
            date: d(2026, 3, 20),
            time: Some(t(15, 0)),
        };
        let s = format_relative_result(&r, crate::datetime::date_math::DateFormat::US);
        assert!(s.contains("3:00 PM"));
        assert!(s.contains("2026"));
    }
}
