//! Unix timestamp conversions: seconds since epoch to/from datetime,
//! with optional timezone support.

use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
use chrono_tz::Tz;

/// The result of converting a unix timestamp.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnixConversion {
    /// The human-readable date.
    pub date: NaiveDate,
    /// The human-readable time.
    pub time: NaiveTime,
    /// Whether the result is in UTC or a named timezone.
    pub tz_label: String,
}

/// Convert a Unix timestamp (seconds since 1970-01-01 00:00:00 UTC) to a
/// human-readable date/time in UTC.
pub fn from_timestamp_utc(ts: i64) -> Option<UnixConversion> {
    let dt = DateTime::from_timestamp(ts, 0)?;
    Some(UnixConversion {
        date: dt.date_naive(),
        time: dt.time(),
        tz_label: "UTC".to_string(),
    })
}

/// Convert a Unix timestamp to a date/time in a specific timezone.
pub fn from_timestamp_in_tz(ts: i64, tz: Tz) -> Option<UnixConversion> {
    let utc_dt = DateTime::from_timestamp(ts, 0)?;
    let local_dt = utc_dt.with_timezone(&tz);
    let tz_label = local_dt.format("%Z").to_string();
    Some(UnixConversion {
        date: local_dt.date_naive(),
        time: local_dt.time(),
        tz_label,
    })
}

/// Convert a UTC date/time to a Unix timestamp.
pub fn to_timestamp_utc(date: NaiveDate, time: NaiveTime) -> Option<i64> {
    let naive = NaiveDateTime::new(date, time);
    Some(naive.and_utc().timestamp())
}

/// Convert a local date/time in a specific timezone to a Unix timestamp.
pub fn to_timestamp_in_tz(date: NaiveDate, time: NaiveTime, tz: Tz) -> Option<i64> {
    let naive = NaiveDateTime::new(date, time);
    let local = tz.from_local_datetime(&naive).single()?;
    Some(local.timestamp())
}

/// Format a `UnixConversion` for display.
pub fn format_unix_conversion(conv: &UnixConversion) -> String {
    let date_str = conv.date.format("%Y-%m-%d").to_string();
    let time_str = conv.time.format("%H:%M:%S").to_string();
    format!("{} {} {}", date_str, time_str, conv.tz_label)
}

/// Format a timestamp as a simple integer string.
pub fn format_timestamp(ts: i64) -> String {
    format!("{}", ts)
}

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

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

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

    // -- from_timestamp_utc --

    #[test]
    fn test_epoch_zero() {
        let result = from_timestamp_utc(0).unwrap();
        assert_eq!(result.date, d(1970, 1, 1));
        assert_eq!(result.time, t(0, 0, 0));
        assert_eq!(result.tz_label, "UTC");
    }

    #[test]
    fn test_known_timestamp() {
        // 1700000000 = 2023-11-14 22:13:20 UTC
        let result = from_timestamp_utc(1_700_000_000).unwrap();
        assert_eq!(result.date, d(2023, 11, 14));
        assert_eq!(result.time, t(22, 13, 20));
    }

    #[test]
    fn test_negative_timestamp() {
        // -86400 = 1969-12-31 00:00:00 UTC
        let result = from_timestamp_utc(-86400).unwrap();
        assert_eq!(result.date, d(1969, 12, 31));
        assert_eq!(result.time, t(0, 0, 0));
    }

    // -- from_timestamp_in_tz --

    #[test]
    fn test_timestamp_in_tokyo() {
        // 0 = 1970-01-01 09:00:00 JST
        let result = from_timestamp_in_tz(0, Tz::Asia__Tokyo).unwrap();
        assert_eq!(result.date, d(1970, 1, 1));
        assert_eq!(result.time, t(9, 0, 0));
        assert_eq!(result.tz_label, "JST");
    }

    // -- to_timestamp_utc --

    #[test]
    fn test_to_timestamp_epoch() {
        let ts = to_timestamp_utc(d(1970, 1, 1), t(0, 0, 0)).unwrap();
        assert_eq!(ts, 0);
    }

    #[test]
    fn test_roundtrip_utc() {
        let original_ts: i64 = 1_700_000_000;
        let conv = from_timestamp_utc(original_ts).unwrap();
        let ts = to_timestamp_utc(conv.date, conv.time).unwrap();
        assert_eq!(ts, original_ts);
    }

    // -- to_timestamp_in_tz --

    #[test]
    fn test_to_timestamp_tokyo() {
        // 1970-01-01 09:00:00 JST should be epoch 0
        let ts = to_timestamp_in_tz(d(1970, 1, 1), t(9, 0, 0), Tz::Asia__Tokyo).unwrap();
        assert_eq!(ts, 0);
    }

    // -- format --

    #[test]
    fn test_format_unix_conversion() {
        let conv = UnixConversion {
            date: d(2023, 11, 14),
            time: t(22, 13, 20),
            tz_label: "UTC".to_string(),
        };
        assert_eq!(format_unix_conversion(&conv), "2023-11-14 22:13:20 UTC");
    }

    #[test]
    fn test_format_timestamp() {
        assert_eq!(format_timestamp(1_700_000_000), "1700000000");
    }
}
