use crate::{BoxFuture, Closure, Result};
use std::{str::FromStr, sync::Arc};

#[derive(Clone)]
pub enum Handler {
    Sync(Closure<Result<()>>),
    Async(Closure<BoxFuture<'static, Result<()>>>),
}

impl Handler {
    pub fn is_async(&self) -> bool {
        match self {
            Handler::Sync(_) => false,
            Handler::Async(_) => true,
        }
    }
}

#[derive(Clone)]
pub struct Task {
    schedule: cron::Schedule,
    handler: Handler,
    last_tick: Option<chrono::DateTime<chrono::Local>>,
}

impl Task {
    fn new(schedule: cron::Schedule, handler: Handler) -> Self {
        Self {
            schedule,
            handler,
            last_tick: None,
        }
    }

    pub fn background<F>(expression: &str, closure: F) -> Self
    where
        F: Fn() -> BoxFuture<'static, Result<()>> + 'static + Send + Sync,
    {
        Self::new(
            cron::Schedule::from_str(expression).unwrap_or_else(|err| panic!("{err}")),
            Handler::Async(Arc::new(closure)),
        )
    }

    pub fn foreground<F>(expression: &str, closure: F) -> Self
    where
        F: Fn() -> Result<()> + 'static + Send + Sync,
    {
        Self::new(
            cron::Schedule::from_str(expression).unwrap_or_else(|err| panic!("{err}")),
            Handler::Sync(Arc::new(closure)),
        )
    }

    pub fn available(&self) -> bool {
        let now = chrono::Local::now();

        let mut available = false;
        if let Some(last_tick) = self.last_tick {
            for event in self.schedule.after(&last_tick) {
                if event > now {
                    break;
                }

                available = true;
            }
        }

        available || self.last_tick.is_none()
    }

    pub fn set_last_tick(&mut self, datetime: chrono::DateTime<chrono::Local>) {
        self.last_tick = Some(datetime);
    }

    pub fn handler(&self) -> &Handler {
        &self.handler
    }

    pub fn delay(&self) -> std::time::Duration {
        let now = chrono::Local::now();
        let mut duration = chrono::Duration::zero();

        for event in self.schedule.after(&now).take(1) {
            let interval = event - now;
            if duration.is_zero() || interval < duration {
                duration = interval;
            }
        }

        duration.to_std().unwrap_or(std::time::Duration::from_millis(500))
    }
}
