use chrono::{Datelike, NaiveDate, NaiveTime, Timelike};

use crate::calendar::TradingCalendar;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScheduleStage {
    BeforeTrading,
    OpenAuction,
    OnDay,
    AfterTrading,
    Settlement,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScheduleTimeRule {
    BeforeTrading,
    MinuteOfDay(u32),
}

impl ScheduleTimeRule {
    pub fn before_trading() -> Self {
        Self::BeforeTrading
    }

    pub fn market_open(hour: i32, minute: i32) -> Self {
        let mut minutes_since_midnight = 9 * 60 + 31 + hour * 60 + minute;
        if minutes_since_midnight > 11 * 60 + 30 {
            minutes_since_midnight += 90;
        }
        Self::MinuteOfDay(minutes_since_midnight.max(0) as u32)
    }

    pub fn market_close(hour: i32, minute: i32) -> Self {
        let mut minutes_since_midnight = 15 * 60 - hour * 60 - minute;
        if minutes_since_midnight < 13 * 60 {
            minutes_since_midnight -= 90;
        }
        Self::MinuteOfDay(minutes_since_midnight.max(0) as u32)
    }

    pub fn physical_time(hour: u32, minute: u32) -> Self {
        Self::MinuteOfDay(hour.saturating_mul(60).saturating_add(minute))
    }

    pub fn from_time(time: NaiveTime) -> Self {
        Self::physical_time(time.hour(), time.minute())
    }

    pub fn minute_of_day(&self) -> Option<u32> {
        match self {
            Self::BeforeTrading => None,
            Self::MinuteOfDay(value) => Some(*value),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScheduleFrequency {
    Daily,
    Weekly {
        weekday: Option<u32>,
        tradingday: Option<i32>,
    },
    Monthly {
        tradingday: i32,
    },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScheduleRule {
    pub name: String,
    pub stage: ScheduleStage,
    pub frequency: ScheduleFrequency,
    pub time_rule: Option<ScheduleTimeRule>,
}

impl ScheduleRule {
    pub fn daily(name: impl Into<String>, stage: ScheduleStage) -> Self {
        Self {
            name: name.into(),
            stage,
            frequency: ScheduleFrequency::Daily,
            time_rule: None,
        }
    }

    pub fn weekly_by_weekday(name: impl Into<String>, weekday: u32, stage: ScheduleStage) -> Self {
        Self {
            name: name.into(),
            stage,
            frequency: ScheduleFrequency::Weekly {
                weekday: Some(weekday),
                tradingday: None,
            },
            time_rule: None,
        }
    }

    pub fn weekly_by_tradingday(
        name: impl Into<String>,
        tradingday: i32,
        stage: ScheduleStage,
    ) -> Self {
        Self {
            name: name.into(),
            stage,
            frequency: ScheduleFrequency::Weekly {
                weekday: None,
                tradingday: Some(tradingday),
            },
            time_rule: None,
        }
    }

    pub fn monthly(name: impl Into<String>, tradingday: i32, stage: ScheduleStage) -> Self {
        Self {
            name: name.into(),
            stage,
            frequency: ScheduleFrequency::Monthly { tradingday },
            time_rule: None,
        }
    }

    pub fn with_time_rule(mut self, time_rule: ScheduleTimeRule) -> Self {
        self.time_rule = Some(time_rule);
        self
    }
}

pub struct Scheduler<'a> {
    calendar: &'a TradingCalendar,
}

impl<'a> Scheduler<'a> {
    pub fn new(calendar: &'a TradingCalendar) -> Self {
        Self { calendar }
    }

    pub fn triggered_rules<'r>(
        &self,
        date: NaiveDate,
        stage: ScheduleStage,
        rules: &'r [ScheduleRule],
    ) -> Vec<&'r ScheduleRule> {
        self.triggered_rules_at(date, stage, default_stage_time(stage), rules)
    }

    pub fn triggered_rules_at<'r>(
        &self,
        date: NaiveDate,
        stage: ScheduleStage,
        current_time: Option<NaiveTime>,
        rules: &'r [ScheduleRule],
    ) -> Vec<&'r ScheduleRule> {
        rules
            .iter()
            .filter(|rule| {
                rule.stage == stage
                    && self.matches(date, rule)
                    && self.matches_time(stage, current_time, rule.time_rule.as_ref())
            })
            .collect()
    }

    fn matches(&self, date: NaiveDate, rule: &ScheduleRule) -> bool {
        match &rule.frequency {
            ScheduleFrequency::Daily => true,
            ScheduleFrequency::Weekly {
                weekday,
                tradingday,
            } => {
                if let Some(weekday) = weekday {
                    return date.weekday().number_from_monday() == *weekday;
                }
                tradingday
                    .and_then(|nth| nth_date_in_period(&self.week_dates(date), nth))
                    .is_some_and(|matched| matched == date)
            }
            ScheduleFrequency::Monthly { tradingday } => {
                nth_date_in_period(&self.month_dates(date), *tradingday)
                    .is_some_and(|matched| matched == date)
            }
        }
    }

    fn week_dates(&self, date: NaiveDate) -> Vec<NaiveDate> {
        let iso = date.iso_week();
        self.calendar
            .iter()
            .filter(|candidate| candidate.iso_week() == iso)
            .collect()
    }

    fn month_dates(&self, date: NaiveDate) -> Vec<NaiveDate> {
        self.calendar
            .iter()
            .filter(|candidate| {
                candidate.year() == date.year() && candidate.month() == date.month()
            })
            .collect()
    }

    fn matches_time(
        &self,
        stage: ScheduleStage,
        current_time: Option<NaiveTime>,
        time_rule: Option<&ScheduleTimeRule>,
    ) -> bool {
        let Some(time_rule) = time_rule else {
            return true;
        };
        match time_rule {
            ScheduleTimeRule::BeforeTrading => stage == ScheduleStage::BeforeTrading,
            ScheduleTimeRule::MinuteOfDay(expected) => current_time
                .map(|value| value.hour() * 60 + value.minute())
                .is_some_and(|current| current == *expected),
        }
    }
}

pub fn default_stage_time(stage: ScheduleStage) -> Option<NaiveTime> {
    match stage {
        ScheduleStage::BeforeTrading => Some(NaiveTime::from_hms_opt(9, 0, 0).expect("valid time")),
        ScheduleStage::OpenAuction => Some(NaiveTime::from_hms_opt(9, 31, 0).expect("valid time")),
        ScheduleStage::OnDay => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
        ScheduleStage::AfterTrading => Some(NaiveTime::from_hms_opt(15, 0, 0).expect("valid time")),
        ScheduleStage::Settlement => Some(NaiveTime::from_hms_opt(15, 1, 0).expect("valid time")),
    }
}

fn nth_date_in_period(period: &[NaiveDate], nth: i32) -> Option<NaiveDate> {
    if nth == 0 || period.is_empty() {
        return None;
    }
    if nth > 0 {
        period.get((nth - 1) as usize).copied()
    } else {
        let idx = period.len().checked_sub((-nth) as usize)?;
        period.get(idx).copied()
    }
}

#[cfg(test)]
mod tests {
    use chrono::{NaiveDate, NaiveTime};

    use super::{ScheduleRule, ScheduleStage, ScheduleTimeRule, Scheduler};
    use crate::calendar::TradingCalendar;

    fn d(year: i32, month: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("valid date")
    }

    fn sample_calendar() -> TradingCalendar {
        TradingCalendar::new(vec![
            d(2025, 1, 30),
            d(2025, 1, 31),
            d(2025, 2, 3),
            d(2025, 2, 4),
            d(2025, 2, 7),
        ])
    }

    #[test]
    fn scheduler_matches_daily_weekly_and_monthly_rules() {
        let calendar = sample_calendar();
        let scheduler = Scheduler::new(&calendar);
        let rules = vec![
            ScheduleRule::daily("daily", ScheduleStage::OnDay),
            ScheduleRule::weekly_by_weekday("friday", 5, ScheduleStage::OnDay),
            ScheduleRule::weekly_by_tradingday(
                "last_trading_day_of_week",
                -1,
                ScheduleStage::OnDay,
            ),
            ScheduleRule::monthly("first_trading_day_of_month", 1, ScheduleStage::OnDay),
        ];

        let jan_31 = scheduler
            .triggered_rules(d(2025, 1, 31), ScheduleStage::OnDay, &rules)
            .into_iter()
            .map(|rule| rule.name.as_str())
            .collect::<Vec<_>>();
        assert!(jan_31.contains(&"daily"));
        assert!(jan_31.contains(&"friday"));
        assert!(jan_31.contains(&"last_trading_day_of_week"));
        assert!(!jan_31.contains(&"first_trading_day_of_month"));

        let feb_3 = scheduler
            .triggered_rules(d(2025, 2, 3), ScheduleStage::OnDay, &rules)
            .into_iter()
            .map(|rule| rule.name.as_str())
            .collect::<Vec<_>>();
        assert!(feb_3.contains(&"daily"));
        assert!(feb_3.contains(&"first_trading_day_of_month"));
        assert!(!feb_3.contains(&"friday"));
    }

    #[test]
    fn scheduler_matches_before_trading_and_minute_level_rules() {
        let calendar = sample_calendar();
        let scheduler = Scheduler::new(&calendar);
        let rules = vec![
            ScheduleRule::daily("before_trading", ScheduleStage::BeforeTrading)
                .with_time_rule(ScheduleTimeRule::before_trading()),
            ScheduleRule::daily("market_open", ScheduleStage::OpenAuction)
                .with_time_rule(ScheduleTimeRule::market_open(0, 0)),
            ScheduleRule::daily("physical_time", ScheduleStage::OnDay)
                .with_time_rule(ScheduleTimeRule::physical_time(10, 18)),
            ScheduleRule::daily("market_close", ScheduleStage::AfterTrading)
                .with_time_rule(ScheduleTimeRule::market_close(0, 0)),
        ];

        let before = scheduler
            .triggered_rules_at(
                d(2025, 2, 3),
                ScheduleStage::BeforeTrading,
                Some(NaiveTime::from_hms_opt(9, 0, 0).unwrap()),
                &rules,
            )
            .into_iter()
            .map(|rule| rule.name.as_str())
            .collect::<Vec<_>>();
        assert_eq!(before, vec!["before_trading"]);

        let market_open = scheduler
            .triggered_rules_at(
                d(2025, 2, 3),
                ScheduleStage::OpenAuction,
                Some(NaiveTime::from_hms_opt(9, 31, 0).unwrap()),
                &rules,
            )
            .into_iter()
            .map(|rule| rule.name.as_str())
            .collect::<Vec<_>>();
        assert_eq!(market_open, vec!["market_open"]);

        let intraday = scheduler
            .triggered_rules_at(
                d(2025, 2, 3),
                ScheduleStage::OnDay,
                Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()),
                &rules,
            )
            .into_iter()
            .map(|rule| rule.name.as_str())
            .collect::<Vec<_>>();
        assert_eq!(intraday, vec!["physical_time"]);

        let close = scheduler
            .triggered_rules_at(
                d(2025, 2, 3),
                ScheduleStage::AfterTrading,
                Some(NaiveTime::from_hms_opt(15, 0, 0).unwrap()),
                &rules,
            )
            .into_iter()
            .map(|rule| rule.name.as_str())
            .collect::<Vec<_>>();
        assert_eq!(close, vec!["market_close"]);

        let mismatch = scheduler
            .triggered_rules_at(
                d(2025, 2, 3),
                ScheduleStage::OnDay,
                Some(NaiveTime::from_hms_opt(10, 17, 0).unwrap()),
                &rules,
            )
            .into_iter()
            .map(|rule| rule.name.as_str())
            .collect::<Vec<_>>();
        assert!(mismatch.is_empty());
    }
}
