use time::format_description::well_known::Rfc3339;
use time::{OffsetDateTime, UtcOffset};

/// Current UTC time as an RFC3339 string with second precision, e.g.
/// `2026-04-15T10:32:00Z`. Subsecond fractions are deliberately stripped
/// so every caller produces the same filename shape — the server's
/// filename parser expects exactly `YYYY-MM-DDTHH-MM-SSZ` after the
/// `:` → `-` substitution, and dictation events don't need finer
/// resolution than one second.
pub fn now_rfc3339() -> String {
    OffsetDateTime::now_utc()
        .replace_nanosecond(0)
        .expect("0 is always a valid nanosecond value")
        .format(&Rfc3339)
        .expect("RFC3339 formatting of OffsetDateTime is infallible")
}

/// Map an RFC3339 timestamp to a filesystem-safe stem by replacing every
/// `:` with `-`. Intentionally naive — RFC3339 has no colons outside the
/// time/offset portion, so the result is bijective against
/// `filename_stem_to_recorded_at`.
///
/// Example: `"2026-04-15T10:32:00Z"` → `"2026-04-15T10-32-00Z"`.
pub fn recorded_at_to_filename_stem(recorded_at: &str) -> String {
    recorded_at.replace(':', "-")
}

/// Reverse of `recorded_at_to_filename_stem`. Only undoes the mapping in
/// the time portion (after `T`) so date-part hyphens stay intact.
///
/// Example: `"2026-04-15T10-32-00Z"` → `"2026-04-15T10:32:00Z"`.
pub fn filename_stem_to_recorded_at(stem: &str) -> String {
    match stem.split_once('T') {
        Some((date, time)) => format!("{}T{}", date, time.replace('-', ":")),
        None => stem.to_string(),
    }
}

/// Parse an RFC3339 timestamp. Returns `None` on any error so callers
/// can fall back to a lenient display path without having to classify
/// failures.
pub fn parse_rfc3339(s: &str) -> Option<OffsetDateTime> {
    OffsetDateTime::parse(s, &Rfc3339).ok()
}

/// Extract `HH:MM` in the local timezone from an RFC3339 UTC timestamp.
/// On parse failure, returns the raw string truncated to its first 16
/// characters — the user still sees something readable even if the
/// value is shaped unexpectedly.
pub fn extract_hhmm(ts: &str) -> String {
    match parse_rfc3339(ts) {
        Some(t) => {
            let local = t.to_offset(UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC));
            format!("{:02}:{:02}", local.hour(), local.minute())
        }
        None => ts.chars().take(16).collect(),
    }
}

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

    #[test]
    fn filename_stem_roundtrip() {
        assert_eq!(
            filename_stem_to_recorded_at("2026-04-15T10-32-00Z"),
            "2026-04-15T10:32:00Z"
        );
        assert_eq!(
            filename_stem_to_recorded_at("2026-04-15T10-32-00.123Z"),
            "2026-04-15T10:32:00.123Z"
        );
    }

    #[test]
    fn recorded_at_stem_roundtrip_utc() {
        let original = "2026-04-15T10:32:00Z";
        let stem = recorded_at_to_filename_stem(original);
        assert_eq!(stem, "2026-04-15T10-32-00Z");
        assert_eq!(filename_stem_to_recorded_at(&stem), original);
    }

    #[test]
    fn recorded_at_stem_roundtrip_with_milliseconds() {
        let original = "2026-04-15T10:32:00.123Z";
        let stem = recorded_at_to_filename_stem(original);
        assert_eq!(filename_stem_to_recorded_at(&stem), original);
    }

    #[test]
    fn now_rfc3339_has_expected_shape() {
        let now = now_rfc3339();
        assert!(now.starts_with("20"), "unexpected year prefix: {now}");
        assert!(now.contains('T'), "missing T separator: {now}");
    }

    /// Regression guard: subsecond precision must be stripped. A single
    /// nanosecond leak would feed the server filename parser
    /// `YYYY-MM-DDTHH-MM-SS.nnnnnnnnnZ.m4a`, which it treats as
    /// malformed — the resulting `<time datetime>` is empty, breaking
    /// browser-side TZ rendering.
    #[test]
    fn now_rfc3339_has_no_subsecond_component() {
        let now = now_rfc3339();
        assert!(
            !now.contains('.'),
            "now_rfc3339 must be second-granular, got {now}"
        );
        assert!(now.ends_with('Z'), "expected UTC 'Z' suffix, got {now}");
    }

    #[test]
    fn parse_rfc3339_accepts_utc_z() {
        let t = parse_rfc3339("2026-04-18T10:32:00Z").expect("parse UTC");
        assert_eq!(t.hour(), 10);
        assert_eq!(t.minute(), 32);
    }

    #[test]
    fn parse_rfc3339_returns_none_on_garbage() {
        assert!(parse_rfc3339("not a timestamp").is_none());
    }

    /// `extract_hhmm` depends on the host timezone for formatting, so we
    /// can't assert an exact value — but we can assert the shape and
    /// that the fallback kicks in on unparseable input.
    #[test]
    fn extract_hhmm_shape_is_two_colon_two() {
        let s = extract_hhmm("2026-04-18T10:32:00Z");
        assert_eq!(s.len(), 5, "expected HH:MM, got {s}");
        assert!(
            s.chars().nth(2) == Some(':'),
            "expected colon at index 2: {s}"
        );
    }

    #[test]
    fn extract_hhmm_falls_back_to_truncated_raw_on_parse_failure() {
        assert_eq!(extract_hhmm("not-a-timestamp"), "not-a-timestamp");
        assert_eq!(
            extract_hhmm("way-longer-than-sixteen-chars"),
            "way-longer-than-"
        );
    }
}
