//! Expiry parsing and checking utilities for time-limited acknowledgments.
//!
//! Supports two formats:
//! - Duration: "90d" (days from now)
//! - ISO 8601 date: "2026-12-31"
//!
//! # Example
//!
//! ```ignore
//! use aphoria::expiry::{parse_expiry, is_expired, format_expiry};
//!
//! // Parse duration format
//! let expires_at = parse_expiry("90d")?;
//! assert!(!is_expired(expires_at));
//!
//! // Parse ISO date format
//! let expires_at = parse_expiry("2030-12-31")?;
//! assert!(!is_expired(expires_at));
//!
//! // Format for display
//! println!("Expires: {}", format_expiry(expires_at));
//! ```

use chrono::{NaiveDate, TimeZone, Utc};

use crate::current_timestamp;
use crate::error::AphoriaError;

/// Parse an expiry specification into a Unix timestamp (seconds since epoch).
///
/// # Supported formats
///
/// - Duration: `"90d"` - 90 days from now (must be positive)
/// - ISO 8601 date: `"2026-12-31"` - specific date at midnight UTC
///
/// # Errors
///
/// Returns `AphoriaError::InvalidExpiry` if:
/// - Format is unrecognized
/// - Duration is zero or negative
/// - Date is in the past
/// - Date format is invalid
pub fn parse_expiry(spec: &str) -> Result<u64, AphoriaError> {
    let spec = spec.trim();

    // Try duration format first (e.g., "90d")
    if let Some(stripped) = spec.strip_suffix('d') {
        let days: u32 = stripped.parse().map_err(|_| {
            AphoriaError::InvalidExpiry(format!(
                "invalid duration '{}': expected format like '90d'",
                spec
            ))
        })?;

        if days == 0 {
            return Err(AphoriaError::InvalidExpiry(
                "expiry duration must be at least 1 day".to_string(),
            ));
        }

        // Bounds check to prevent timestamp overflow (~100 years max)
        if days > 36500 {
            return Err(AphoriaError::InvalidExpiry(
                "expiry duration too large (max 36500 days / ~100 years)".to_string(),
            ));
        }

        let now = Utc::now();
        let expires = now + chrono::Duration::days(i64::from(days));
        return Ok(expires.timestamp() as u64);
    }

    // Try ISO 8601 date format (e.g., "2026-12-31")
    let date = NaiveDate::parse_from_str(spec, "%Y-%m-%d").map_err(|e| {
        AphoriaError::InvalidExpiry(format!(
            "invalid date '{}': expected ISO 8601 format (YYYY-MM-DD). {}",
            spec, e
        ))
    })?;

    // Convert to midnight UTC
    let datetime = date
        .and_hms_opt(0, 0, 0)
        .ok_or_else(|| AphoriaError::InvalidExpiry("invalid time component".to_string()))?;

    let expires = Utc.from_utc_datetime(&datetime);
    let now = Utc::now();

    if expires <= now {
        return Err(AphoriaError::InvalidExpiry(format!(
            "date '{}' is in the past (current date is {})",
            spec,
            now.format("%Y-%m-%d")
        )));
    }

    Ok(expires.timestamp() as u64)
}

/// Check if an expiry timestamp is in the past.
///
/// # Arguments
///
/// * `expires_at` - Unix timestamp (seconds since epoch)
///
/// # Returns
///
/// `true` if the timestamp is in the past, `false` otherwise.
pub fn is_expired(expires_at: u64) -> bool {
    expires_at <= current_timestamp()
}

/// Format an expiry timestamp as an ISO 8601 date string.
///
/// # Arguments
///
/// * `expires_at` - Unix timestamp (seconds since epoch)
///
/// # Returns
///
/// ISO 8601 formatted date string (e.g., "2026-12-31")
pub fn format_expiry(expires_at: u64) -> String {
    match chrono::DateTime::from_timestamp(expires_at as i64, 0) {
        Some(dt) => dt.format("%Y-%m-%d").to_string(),
        None => format!("invalid-timestamp-{}", expires_at),
    }
}

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

    #[test]
    fn test_parse_duration_90d() {
        let result = parse_expiry("90d");
        assert!(result.is_ok());

        let expires_at = result.expect("should parse");
        let now = Utc::now().timestamp() as u64;

        // Should be approximately 90 days from now (with small tolerance)
        let expected_min = now + (89 * 24 * 60 * 60);
        let expected_max = now + (91 * 24 * 60 * 60);

        assert!(
            expires_at >= expected_min && expires_at <= expected_max,
            "expires_at {} should be within 89-91 days from now ({}..{})",
            expires_at,
            expected_min,
            expected_max
        );
    }

    #[test]
    fn test_parse_duration_1d() {
        let result = parse_expiry("1d");
        assert!(result.is_ok());

        let expires_at = result.expect("should parse");
        let now = Utc::now().timestamp() as u64;

        // Should be approximately 1 day from now
        let expected_min = now + (23 * 60 * 60);
        let expected_max = now + (25 * 60 * 60);

        assert!(expires_at >= expected_min && expires_at <= expected_max);
    }

    #[test]
    fn test_parse_iso_date() {
        // Use a date far in the future to avoid test failures
        let result = parse_expiry("2099-12-31");
        assert!(result.is_ok());

        let expires_at = result.expect("should parse");
        let formatted = format_expiry(expires_at);
        assert_eq!(formatted, "2099-12-31");
    }

    #[test]
    fn test_zero_duration_fails() {
        let result = parse_expiry("0d");
        assert!(result.is_err());

        let err = result.unwrap_err();
        assert!(matches!(err, AphoriaError::InvalidExpiry(msg) if msg.contains("at least 1 day")));
    }

    #[test]
    fn test_past_date_fails() {
        let result = parse_expiry("2020-01-01");
        assert!(result.is_err());

        let err = result.unwrap_err();
        assert!(matches!(err, AphoriaError::InvalidExpiry(msg) if msg.contains("past")));
    }

    #[test]
    fn test_invalid_format() {
        let result = parse_expiry("forever");
        assert!(result.is_err());

        let err = result.unwrap_err();
        assert!(matches!(err, AphoriaError::InvalidExpiry(_)));
    }

    #[test]
    fn test_invalid_date_format() {
        let result = parse_expiry("12-31-2026");
        assert!(result.is_err());

        let err = result.unwrap_err();
        assert!(matches!(err, AphoriaError::InvalidExpiry(msg) if msg.contains("ISO 8601")));
    }

    #[test]
    fn test_is_expired_past() {
        // A timestamp from the past
        let past = Utc::now().timestamp() as u64 - 1000;
        assert!(is_expired(past));
    }

    #[test]
    fn test_is_expired_future() {
        // A timestamp in the future
        let future = Utc::now().timestamp() as u64 + 1000;
        assert!(!is_expired(future));
    }

    #[test]
    fn test_format_expiry() {
        // Use chrono to create a known timestamp
        let date = NaiveDate::from_ymd_opt(2099, 6, 15).expect("valid date");
        let datetime = date.and_hms_opt(0, 0, 0).expect("valid time");
        let dt = Utc.from_utc_datetime(&datetime);
        let ts = dt.timestamp() as u64;

        assert_eq!(format_expiry(ts), "2099-06-15");
    }

    #[test]
    fn test_whitespace_trimmed() {
        let result = parse_expiry("  90d  ");
        assert!(result.is_ok());
    }

    #[test]
    fn test_negative_duration_fails() {
        let result = parse_expiry("-5d");
        assert!(result.is_err());
    }
}
