/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
 *
 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
 */

use chrono::{Datelike, Local, TimeDelta, TimeZone, Timelike};
use std::{str::FromStr, time::Duration};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SimpleCron {
    Day { hour: u32, minute: u32 },
    Week { day: u32, hour: u32, minute: u32 },
    Hour { minute: u32 },
}

impl SimpleCron {
    pub fn time_to_next(&self) -> Duration {
        let now = Local::now();
        let next = match self {
            SimpleCron::Day { hour, minute } => {
                let next = Local
                    .with_ymd_and_hms(now.year(), now.month(), now.day(), *hour, *minute, 0)
                    .earliest()
                    .unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
                if next <= now {
                    next + TimeDelta::try_days(1).unwrap_or_default()
                } else {
                    next
                }
            }
            SimpleCron::Week { day, hour, minute } => {
                let next = Local
                    .with_ymd_and_hms(now.year(), now.month(), now.day(), *hour, *minute, 0)
                    .earliest()
                    .unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
                if next <= now {
                    next + TimeDelta::try_days(
                        (7 - now.weekday().number_from_monday() + *day).into(),
                    )
                    .unwrap_or_default()
                } else {
                    next
                }
            }
            SimpleCron::Hour { minute } => {
                let next = Local
                    .with_ymd_and_hms(now.year(), now.month(), now.day(), now.hour(), *minute, 0)
                    .earliest()
                    .unwrap_or_else(|| now - TimeDelta::try_seconds(1).unwrap_or_default());
                if next <= now {
                    next + TimeDelta::try_hours(1).unwrap_or_default()
                } else {
                    next
                }
            }
        };

        (next - now).to_std().unwrap_or_else(|_| self.as_duration())
    }

    pub fn as_duration(&self) -> Duration {
        match self {
            SimpleCron::Day { .. } => Duration::from_secs(24 * 60 * 60),
            SimpleCron::Week { .. } => Duration::from_secs(7 * 24 * 60 * 60),
            SimpleCron::Hour { .. } => Duration::from_secs(60 * 60),
        }
    }
}

impl FromStr for SimpleCron {
    type Err = String;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        let mut hour = 0;
        let mut minute = 0;

        for (pos, value) in value.split(' ').enumerate() {
            if pos == 0 {
                minute = value
                    .parse::<u32>()
                    .map_err(|_| "Invalid cron key: failed to parse cron minute".to_string())?;
                if !(0..=59).contains(&minute) {
                    return Err(format!(
                        "Invalid cron key: failed to parse minute, invalid value: {minute}"
                    ));
                }
            } else if pos == 1 {
                if value
                    .as_bytes()
                    .first()
                    .ok_or_else(|| "Invalid cron key: failed to parse cron hour".to_string())?
                    == &b'*'
                {
                    return Ok(SimpleCron::Hour { minute });
                } else {
                    hour = value
                        .parse::<u32>()
                        .map_err(|_| "Invalid cron key: failed to parse cron hour".to_string())?;
                    if !(0..=23).contains(&hour) {
                        return Err(format!(
                            "Invalid cron key: failed to parse hour, invalid value: {hour}"
                        ));
                    }
                }
            } else if pos == 2 {
                if value
                    .as_bytes()
                    .first()
                    .ok_or_else(|| "Invalid cron key: failed to parse cron weekday".to_string())?
                    == &b'*'
                {
                    return Ok(SimpleCron::Day { hour, minute });
                } else {
                    let day = value.parse::<u32>().map_err(|_| {
                        "Invalid cron key: failed to parse cron weekday".to_string()
                    })?;
                    if !(1..=7).contains(&hour) {
                        return Err(format!(
                            "Invalid cron key: failed to parse weekday, invalid value: {}, range is 1 (Monday) to 7 (Sunday).",
                            hour,
                        ));
                    }

                    return Ok(SimpleCron::Week { day, hour, minute });
                }
            }
        }

        Err("Invalid cron key: parse cron expression.".to_string())
    }
}

impl Default for SimpleCron {
    fn default() -> Self {
        SimpleCron::Hour { minute: 0 }
    }
}
