//! Small scheduling primitives built on top of `athena-worker`.
//!
//! Use this crate when you need:
//!
//! - a long-running polling loop with separate idle/busy/error intervals, or
//! - wall-clock scheduling helpers that turn backup-style frequencies into cron
//!   expressions and next-run timestamps.

use std::fmt;
use std::future::Future;
use std::time::Duration;

use athena_worker::runtime::{WorkerLoopConfig, WorkerTickResult, run_worker_loop};
use chrono::{DateTime, Utc};
use chrono::{Datelike, Timelike};

pub use athena_worker::runtime::WorkerTickResult as SchedulerTickResult;

/// Runtime cadence for a long-lived scheduler worker loop.
#[derive(Debug, Clone, Copy)]
pub struct SchedulerLoopConfig {
    pub worker_name: &'static str,
    pub poll_interval: Duration,
    pub busy_interval: Duration,
    pub error_interval: Duration,
}

impl SchedulerLoopConfig {
    pub fn new(worker_name: &'static str, poll_interval: Duration) -> Self {
        Self {
            worker_name,
            poll_interval,
            busy_interval: Duration::from_secs(1),
            error_interval: poll_interval,
        }
    }

    pub fn busy_interval(mut self, interval: Duration) -> Self {
        self.busy_interval = interval;
        self
    }

    pub fn error_interval(mut self, interval: Duration) -> Self {
        self.error_interval = interval;
        self
    }

    fn worker_loop_config(self) -> WorkerLoopConfig {
        WorkerLoopConfig {
            poll_interval: self.poll_interval,
            busy_interval: self.busy_interval,
            error_interval: self.error_interval,
        }
    }
}

pub async fn run_scheduler_loop<F, Fut, E>(config: SchedulerLoopConfig, tick: F) -> !
where
    F: FnMut() -> Fut,
    Fut: Future<Output = Result<WorkerTickResult, E>>,
    E: fmt::Display,
{
    run_worker_loop(config.worker_name, config.worker_loop_config(), tick).await
}

/// Frequency choices supported by [`WallClockSchedule`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScheduleFrequency {
    Hourly,
    Daily,
    Weekly,
    Monthly,
}

impl ScheduleFrequency {
    pub fn parse(value: &str) -> Result<Self, ScheduleError> {
        match value {
            "hourly" => Ok(Self::Hourly),
            "daily" => Ok(Self::Daily),
            "weekly" => Ok(Self::Weekly),
            "monthly" => Ok(Self::Monthly),
            _ => Err(invalid_frequency(value)),
        }
    }
}

fn invalid_frequency(value: &str) -> ScheduleError {
    ScheduleError::InvalidFrequency(value.to_owned())
}

/// Errors returned when parsing scheduling input.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScheduleError {
    InvalidFrequency(String),
    InvalidTime(String),
}

impl fmt::Display for ScheduleError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidFrequency(value) => write!(f, "Invalid frequency: {value}"),
            Self::InvalidTime(value) => write!(f, "Invalid time: {value}"),
        }
    }
}

impl std::error::Error for ScheduleError {}

/// Parsed backup-style wall-clock schedule.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WallClockSchedule {
    pub frequency: ScheduleFrequency,
    pub hour: u32,
    pub minute: u32,
    pub day_of_week: Option<u32>,
    pub day_of_month: Option<u32>,
}

impl WallClockSchedule {
    pub fn parse(
        frequency: &str,
        time: &str,
        day_of_week: Option<i32>,
        day_of_month: Option<i32>,
    ) -> Result<Self, ScheduleError> {
        let (hour, minute) = parse_time_or_default(time)?;
        Ok(Self {
            frequency: ScheduleFrequency::parse(frequency)?,
            hour,
            minute,
            day_of_week: day_of_week.and_then(|value| u32::try_from(value).ok()),
            day_of_month: day_of_month.and_then(|value| u32::try_from(value).ok()),
        })
    }

    pub fn cron_expression(self) -> String {
        match self.frequency {
            ScheduleFrequency::Hourly => format!("{} * * * *", self.minute),
            ScheduleFrequency::Daily => format!("{} {} * * *", self.minute, self.hour),
            ScheduleFrequency::Weekly => format!(
                "{} {} * * {}",
                self.minute,
                self.hour,
                self.day_of_week.unwrap_or(1)
            ),
            ScheduleFrequency::Monthly => format!(
                "{} {} {} * *",
                self.minute,
                self.hour,
                self.day_of_month.unwrap_or(1)
            ),
        }
    }

    pub fn next_run_after(self, now: DateTime<Utc>) -> DateTime<Utc> {
        let mut candidate = now
            .with_hour(self.hour)
            .unwrap_or(now)
            .with_minute(self.minute)
            .unwrap_or(now)
            .with_second(0)
            .unwrap_or(now);
        if candidate <= now {
            candidate += chrono::Duration::days(1);
        }

        match self.frequency {
            ScheduleFrequency::Weekly => {
                let target = self.day_of_week.unwrap_or(1);
                while Datelike::weekday(&candidate).num_days_from_sunday() != target {
                    candidate += chrono::Duration::days(1);
                }
            }
            ScheduleFrequency::Monthly => {
                let target = self.day_of_month.unwrap_or(1);
                while Datelike::day(&candidate) != target {
                    candidate += chrono::Duration::days(1);
                }
            }
            ScheduleFrequency::Hourly | ScheduleFrequency::Daily => {}
        }

        candidate
    }
}

/// Derive a cron expression from backup-style frequency fields.
pub fn cron_expression_for(
    frequency: &str,
    time: &str,
    day_of_week: Option<i32>,
    day_of_month: Option<i32>,
) -> Result<String, ScheduleError> {
    Ok(WallClockSchedule::parse(frequency, time, day_of_week, day_of_month)?.cron_expression())
}

/// Compute the next run time for a parsed wall-clock schedule.
pub fn next_run_after(
    frequency: &str,
    time: &str,
    day_of_week: Option<i32>,
    day_of_month: Option<i32>,
    now: DateTime<Utc>,
) -> Result<DateTime<Utc>, ScheduleError> {
    Ok(WallClockSchedule::parse(frequency, time, day_of_week, day_of_month)?.next_run_after(now))
}

fn parse_time_or_default(time: &str) -> Result<(u32, u32), ScheduleError> {
    let parts: Vec<&str> = time.split(':').collect();
    if parts.len() != 2 {
        return Ok((2, 0));
    }

    let hour: u32 = parts[0]
        .parse::<u32>()
        .map_err(|_| ScheduleError::InvalidTime(time.to_string()))?;
    let minute: u32 = parts[1]
        .parse::<u32>()
        .map_err(|_| ScheduleError::InvalidTime(time.to_string()))?;
    if hour > 23 || minute > 59 {
        return Err(ScheduleError::InvalidTime(time.to_string()));
    }
    Ok((hour, minute))
}

#[cfg(test)]
mod tests {
    use super::{SchedulerTickResult, WallClockSchedule, cron_expression_for, next_run_after};
    use chrono::Datelike;
    use chrono::{TimeZone, Utc};

    #[test]
    fn scheduler_tick_result_maps_to_worker_result() {
        let result: athena_worker::runtime::WorkerTickResult = SchedulerTickResult::Progress;
        assert_eq!(result, athena_worker::runtime::WorkerTickResult::Progress);
    }

    #[test]
    fn cron_expression_matches_backup_schedule_shape() {
        assert_eq!(
            cron_expression_for("weekly", "03:15", Some(2), None).unwrap(),
            "15 3 * * 2"
        );
        assert_eq!(
            cron_expression_for("monthly", "01:05", None, Some(12)).unwrap(),
            "5 1 12 * *"
        );
    }

    #[test]
    fn next_run_after_moves_due_daily_schedule_to_tomorrow() {
        let now: chrono::prelude::DateTime<Utc> =
            Utc.with_ymd_and_hms(2026, 5, 20, 4, 0, 0).unwrap();
        let next: chrono::prelude::DateTime<Utc> =
            next_run_after("daily", "03:30", None, None, now).unwrap();
        assert_eq!(next, Utc.with_ymd_and_hms(2026, 5, 21, 3, 30, 0).unwrap());
    }

    #[test]
    fn next_run_after_respects_weekly_target() {
        let now: chrono::prelude::DateTime<Utc> =
            Utc.with_ymd_and_hms(2026, 5, 20, 1, 0, 0).unwrap();
        let schedule: WallClockSchedule =
            WallClockSchedule::parse("weekly", "02:00", Some(5), None).unwrap();
        let next: chrono::prelude::DateTime<Utc> = schedule.next_run_after(now);
        assert_eq!(Datelike::weekday(&next).num_days_from_sunday(), 5);
    }
}
