//! Time arithmetic: 3:35 AM + 9h20m, duration between times, 12/24-hour display.

use chrono::{NaiveTime, Timelike};

/// User preference for time display.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum TimeFormat {
    /// 12-hour with AM/PM (e.g., 3:35 PM)
    #[default]
    TwelveHour,
    /// 24-hour (e.g., 15:35)
    TwentyFourHour,
}

/// A time-only duration in hours and minutes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimeDuration {
    pub hours: i64,
    pub minutes: i64,
}

impl TimeDuration {
    pub fn zero() -> Self {
        Self {
            hours: 0,
            minutes: 0,
        }
    }

    /// Total signed minutes.
    pub fn total_minutes(&self) -> i64 {
        self.hours * 60 + self.minutes
    }
}

impl std::fmt::Display for TimeDuration {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut parts = Vec::new();
        if self.hours != 0 {
            let label = if self.hours == 1 { "hour" } else { "hours" };
            parts.push(format!("{} {}", self.hours, label));
        }
        if self.minutes != 0 {
            let label = if self.minutes == 1 {
                "minute"
            } else {
                "minutes"
            };
            parts.push(format!("{} {}", self.minutes, label));
        }
        if parts.is_empty() {
            write!(f, "0 minutes")
        } else {
            write!(f, "{}", parts.join(" "))
        }
    }
}

/// Result of adding/subtracting a duration to/from a time.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimeResult {
    pub time: NaiveTime,
    /// How many days the result has rolled past midnight.
    /// +1 = next day, -1 = previous day, 0 = same day.
    pub day_offset: i32,
}

/// Add hours and minutes to a time, returning the result and any day overflow.
pub fn add_time_duration(time: NaiveTime, hours: i64, minutes: i64) -> TimeResult {
    let time_minutes = time.hour() as i64 * 60 + time.minute() as i64;
    let total_add = hours * 60 + minutes;
    let result_minutes = time_minutes + total_add;

    let day_offset = result_minutes.div_euclid(24 * 60) as i32;
    let normalized = result_minutes.rem_euclid(24 * 60);

    let hour = (normalized / 60) as u32;
    let minute = (normalized % 60) as u32;

    TimeResult {
        time: NaiveTime::from_hms_opt(hour, minute, 0).unwrap_or(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
        day_offset,
    }
}

/// Subtract hours and minutes from a time.
pub fn sub_time_duration(time: NaiveTime, hours: i64, minutes: i64) -> TimeResult {
    add_time_duration(time, -hours, -minutes)
}

/// Calculate the duration between two times. If `to` < `from`, assumes midnight
/// crossing (i.e. `to` is on the next day).
pub fn duration_between(from: NaiveTime, to: NaiveTime) -> TimeDuration {
    let m1 = from.hour() as i64 * 60 + from.minute() as i64;
    let m2 = to.hour() as i64 * 60 + to.minute() as i64;

    let diff = if m2 >= m1 { m2 - m1 } else { (24 * 60 - m1) + m2 };

    TimeDuration {
        hours: diff / 60,
        minutes: diff % 60,
    }
}

/// Convert a 24-hour value to 12-hour. Returns `(hour_12, is_pm)`.
pub fn to_12h(hour24: u32) -> (u32, bool) {
    match hour24 {
        0 => (12, false),
        1..=11 => (hour24, false),
        12 => (12, true),
        13..=23 => (hour24 - 12, true),
        _ => (hour24, false),
    }
}

/// Parse an AM/PM indicator. Returns `Some(is_pm)` or `None`.
pub fn parse_ampm(word: &str) -> Option<bool> {
    match word.to_lowercase().as_str() {
        "am" | "a" => Some(false),
        "pm" | "p" => Some(true),
        _ => None,
    }
}

/// Convert 12-hour time to 24-hour.
pub fn to_24h(hour12: u32, is_pm: bool) -> u32 {
    if is_pm {
        if hour12 == 12 {
            12
        } else {
            hour12 + 12
        }
    } else if hour12 == 12 {
        0
    } else {
        hour12
    }
}

/// Format a `NaiveTime` according to the user's preference.
pub fn format_time(time: NaiveTime, format: TimeFormat) -> String {
    match format {
        TimeFormat::TwelveHour => {
            let (h12, is_pm) = to_12h(time.hour());
            let ampm = if is_pm { "PM" } else { "AM" };
            format!("{}:{:02} {}", h12, time.minute(), ampm)
        }
        TimeFormat::TwentyFourHour => {
            format!("{}:{:02}", time.hour(), time.minute())
        }
    }
}

/// Format a `TimeResult` with optional day-offset annotation.
pub fn format_time_result(tr: &TimeResult, format: TimeFormat) -> String {
    let time_str = format_time(tr.time, format);
    if tr.day_offset > 0 {
        format!("{} (next day)", time_str)
    } else if tr.day_offset < 0 {
        format!("{} (previous day)", time_str)
    } else {
        time_str
    }
}

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

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

    // -- add / sub --

    #[test]
    fn test_add_no_rollover() {
        // 3:35 AM + 9h20m = 12:55 PM
        let r = add_time_duration(t(3, 35), 9, 20);
        assert_eq!(r.time, t(12, 55));
        assert_eq!(r.day_offset, 0);
    }

    #[test]
    fn test_add_with_rollover() {
        // 3:35 PM (15:35) + 9h20m = 0:55 AM next day
        let r = add_time_duration(t(15, 35), 9, 20);
        assert_eq!(r.time, t(0, 55));
        assert_eq!(r.day_offset, 1);
    }

    #[test]
    fn test_sub_no_rollover() {
        // 14:30 - 2h45m = 11:45
        let r = sub_time_duration(t(14, 30), 2, 45);
        assert_eq!(r.time, t(11, 45));
        assert_eq!(r.day_offset, 0);
    }

    #[test]
    fn test_sub_past_midnight() {
        // 1:00 AM - 3h = 10:00 PM previous day
        let r = sub_time_duration(t(1, 0), 3, 0);
        assert_eq!(r.time, t(22, 0));
        assert_eq!(r.day_offset, -1);
    }

    // -- duration_between --

    #[test]
    fn test_duration_workday() {
        // 9:00 AM to 5:30 PM = 8h30m
        let d = duration_between(t(9, 0), t(17, 30));
        assert_eq!(d.hours, 8);
        assert_eq!(d.minutes, 30);
    }

    #[test]
    fn test_duration_midnight_crossing() {
        // 11:00 PM to 2:00 AM = 3h
        let d = duration_between(t(23, 0), t(2, 0));
        assert_eq!(d.hours, 3);
        assert_eq!(d.minutes, 0);
    }

    #[test]
    fn test_duration_same_time() {
        let d = duration_between(t(9, 0), t(9, 0));
        assert_eq!(d.hours, 0);
        assert_eq!(d.minutes, 0);
    }

    // -- 12h/24h conversion --

    #[test]
    fn test_to_12h_midnight() {
        assert_eq!(to_12h(0), (12, false));
    }

    #[test]
    fn test_to_12h_noon() {
        assert_eq!(to_12h(12), (12, true));
    }

    #[test]
    fn test_to_12h_afternoon() {
        assert_eq!(to_12h(15), (3, true));
    }

    #[test]
    fn test_to_24h_am() {
        assert_eq!(to_24h(3, false), 3);
        assert_eq!(to_24h(12, false), 0);
    }

    #[test]
    fn test_to_24h_pm() {
        assert_eq!(to_24h(3, true), 15);
        assert_eq!(to_24h(12, true), 12);
    }

    // -- format --

    #[test]
    fn test_format_12h() {
        assert_eq!(format_time(t(12, 55), TimeFormat::TwelveHour), "12:55 PM");
        assert_eq!(format_time(t(0, 0), TimeFormat::TwelveHour), "12:00 AM");
        assert_eq!(format_time(t(15, 35), TimeFormat::TwelveHour), "3:35 PM");
    }

    #[test]
    fn test_format_24h() {
        assert_eq!(format_time(t(11, 45), TimeFormat::TwentyFourHour), "11:45");
        assert_eq!(format_time(t(0, 0), TimeFormat::TwentyFourHour), "0:00");
    }

    #[test]
    fn test_format_time_result_next_day() {
        let tr = TimeResult {
            time: t(0, 55),
            day_offset: 1,
        };
        assert_eq!(
            format_time_result(&tr, TimeFormat::TwelveHour),
            "12:55 AM (next day)"
        );
    }

    #[test]
    fn test_format_time_result_prev_day() {
        let tr = TimeResult {
            time: t(22, 0),
            day_offset: -1,
        };
        assert_eq!(
            format_time_result(&tr, TimeFormat::TwelveHour),
            "10:00 PM (previous day)"
        );
    }

    // -- TimeDuration display --

    #[test]
    fn test_time_duration_display() {
        let d = TimeDuration {
            hours: 8,
            minutes: 30,
        };
        assert_eq!(d.to_string(), "8 hours 30 minutes");
    }

    #[test]
    fn test_time_duration_display_zero() {
        assert_eq!(TimeDuration::zero().to_string(), "0 minutes");
    }

    #[test]
    fn test_time_duration_display_hours_only() {
        let d = TimeDuration {
            hours: 3,
            minutes: 0,
        };
        assert_eq!(d.to_string(), "3 hours");
    }

    #[test]
    fn test_time_duration_display_singular() {
        let d = TimeDuration {
            hours: 1,
            minutes: 1,
        };
        assert_eq!(d.to_string(), "1 hour 1 minute");
    }
}
