use std::sync::LazyLock;

use anyhow::{Context, Result, bail};
use chrono::{Datelike, Duration, Local, Months, TimeZone, Weekday};
use regex::Regex;

pub(crate) static RECURRENCE_INTERVAL: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"^every\s+(\d+)\s+(days?|weeks?|months?|years?)$").expect("valid recurrence regex")
});

pub fn next_occurrence(current: &str, recurrence: &str) -> Result<String> {
    let current = chrono::DateTime::parse_from_rfc3339(current)?.naive_local();
    let rule = recurrence.trim().to_lowercase();
    let next = match rule.as_str() {
        "daily" => current + Duration::days(1),
        "weekdays" => {
            let mut next = current + Duration::days(1);
            while matches!(next.weekday(), Weekday::Sat | Weekday::Sun) {
                next += Duration::days(1);
            }
            next
        }
        "weekly" => current + Duration::weeks(1),
        "monthly" => current
            .checked_add_months(Months::new(1))
            .context("monthly date is out of range")?,
        "yearly" | "annually" => current
            .checked_add_months(Months::new(12))
            .context("yearly date is out of range")?,
        _ => interval_occurrence(current, &rule)?,
    };
    let next = Local
        .from_local_datetime(&next)
        .earliest()
        .context("next occurrence does not exist in the local timezone")?;
    Ok(next.to_rfc3339())
}

fn interval_occurrence(
    current: chrono::NaiveDateTime,
    rule: &str,
) -> Result<chrono::NaiveDateTime> {
    let Some(captures) = RECURRENCE_INTERVAL.captures(rule) else {
        bail!(
            "recurrence must be daily, weekdays, weekly, monthly, yearly, or 'every N days/weeks/months/years'"
        );
    };
    let interval: u32 = captures[1].parse()?;
    if interval == 0 {
        bail!("recurrence interval must be greater than zero");
    }
    match &captures[2] {
        "day" | "days" => Ok(current + Duration::days(i64::from(interval))),
        "week" | "weeks" => Ok(current + Duration::weeks(i64::from(interval))),
        "month" | "months" => current
            .checked_add_months(Months::new(interval))
            .context("monthly date is out of range"),
        _ => current
            .checked_add_months(Months::new(interval.saturating_mul(12)))
            .context("yearly date is out of range"),
    }
}

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

    #[test]
    fn advances_recurring_dates_using_wall_time() {
        let weekday = next_occurrence("2026-08-14T09:00:00+00:00", "weekdays").unwrap();
        assert!(weekday.contains("2026-08-17T09:00:00"));

        let fortnight = next_occurrence("2026-08-16T09:00:00+00:00", "every 2 weeks").unwrap();
        assert!(fortnight.contains("2026-08-30T09:00:00"));
    }

    #[test]
    fn rejects_zero_intervals() {
        assert!(next_occurrence("2026-08-14T09:00:00+00:00", "every 0 days").is_err());
    }
}
