//! Validated IANA timezone type.

use serde::{Deserialize, Serialize};

/// A validated IANA timezone.
///
/// Wraps `chrono_tz::Tz` and guarantees the timezone string was valid at
/// construction time. Use `ValidTimezone::parse()` to create — it returns
/// `None` for empty or unrecognized timezone strings.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ValidTimezone(chrono_tz::Tz);

impl ValidTimezone {
    /// Parse an IANA timezone string. Returns `None` for empty or invalid input.
    pub fn parse(s: &str) -> Option<Self> {
        let trimmed = s.trim();
        if trimmed.is_empty() {
            return None;
        }
        trimmed.parse::<chrono_tz::Tz>().ok().map(Self)
    }

    /// The underlying `chrono_tz::Tz` value.
    pub fn tz(&self) -> chrono_tz::Tz {
        self.0
    }

    /// The IANA name (e.g. "America/New_York").
    pub fn name(&self) -> &str {
        self.0.name()
    }
}

impl std::fmt::Display for ValidTimezone {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.name())
    }
}

impl Serialize for ValidTimezone {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        self.name().serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for ValidTimezone {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let s = String::deserialize(deserializer)?;
        Self::parse(&s)
            .ok_or_else(|| serde::de::Error::custom(format!("invalid IANA timezone: '{s}'")))
    }
}

/// Lenient deserializer for `Option<ValidTimezone>`.
///
/// Use with `#[serde(default, deserialize_with = "...")]` on fields that may
/// contain invalid timezone strings from historical data. Invalid or empty
/// values deserialize as `None` instead of failing the whole record. Each
/// drop is logged at `debug!` so a typo in fresh user config is at least
/// observable in the logs even though the record loads.
pub fn deserialize_option_lenient<'de, D: serde::Deserializer<'de>>(
    deserializer: D,
) -> Result<Option<ValidTimezone>, D::Error> {
    let opt: Option<String> = Option::deserialize(deserializer)?;
    match opt {
        Some(s) => match ValidTimezone::parse(&s) {
            Some(tz) => Ok(Some(tz)),
            None => {
                tracing::debug!(
                    raw = %s,
                    "lenient deserializer dropped invalid IANA timezone string to None"
                );
                Ok(None)
            }
        },
        None => Ok(None),
    }
}

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

    #[test]
    fn parse_valid_timezone() {
        let tz = ValidTimezone::parse("America/New_York").unwrap();
        assert_eq!(tz.name(), "America/New_York");
    }

    #[test]
    fn parse_with_whitespace() {
        let tz = ValidTimezone::parse("  Europe/London  ").unwrap();
        assert_eq!(tz.name(), "Europe/London");
    }

    #[test]
    fn parse_empty_returns_none() {
        assert!(ValidTimezone::parse("").is_none());
        assert!(ValidTimezone::parse("   ").is_none());
    }

    #[test]
    fn parse_invalid_returns_none() {
        assert!(ValidTimezone::parse("NotATimezone").is_none());
        assert!(ValidTimezone::parse("US/FakeCity").is_none());
    }

    #[test]
    fn serde_roundtrip() {
        let tz = ValidTimezone::parse("Asia/Tokyo").unwrap();
        let json = serde_json::to_string(&tz).unwrap();
        assert_eq!(json, "\"Asia/Tokyo\"");
        let back: ValidTimezone = serde_json::from_str(&json).unwrap();
        assert_eq!(back, tz);
    }

    #[test]
    fn deserialize_invalid_fails() {
        let result: Result<ValidTimezone, _> = serde_json::from_str("\"NotReal\"");
        assert!(result.is_err());
    }

    #[test]
    fn lenient_deserialize_valid() {
        #[derive(serde::Deserialize)]
        struct T {
            #[serde(default, deserialize_with = "super::deserialize_option_lenient")]
            tz: Option<ValidTimezone>,
        }
        let t: T = serde_json::from_str(r#"{"tz":"America/Chicago"}"#).unwrap();
        assert_eq!(t.tz.unwrap().name(), "America/Chicago");
    }

    #[test]
    fn lenient_deserialize_invalid_becomes_none() {
        #[derive(serde::Deserialize)]
        struct T {
            #[serde(default, deserialize_with = "super::deserialize_option_lenient")]
            tz: Option<ValidTimezone>,
        }
        let t: T = serde_json::from_str(r#"{"tz":"NotReal"}"#).unwrap();
        assert!(t.tz.is_none(), "invalid timezone should become None");
    }

    #[test]
    fn lenient_deserialize_null_becomes_none() {
        #[derive(serde::Deserialize)]
        struct T {
            #[serde(default, deserialize_with = "super::deserialize_option_lenient")]
            tz: Option<ValidTimezone>,
        }
        let t: T = serde_json::from_str(r#"{"tz":null}"#).unwrap();
        assert!(t.tz.is_none());
    }

    #[test]
    fn lenient_deserialize_missing_becomes_none() {
        #[derive(serde::Deserialize)]
        struct T {
            #[serde(default, deserialize_with = "super::deserialize_option_lenient")]
            tz: Option<ValidTimezone>,
        }
        let t: T = serde_json::from_str(r#"{}"#).unwrap();
        assert!(t.tz.is_none());
    }
}
