use chrono::{DateTime, Timelike};
use serde::Serialize;

use crate::{Language, Timezone};

// A definition of hours (inclusive)
#[derive(Debug, Clone, Copy, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Hours(u8, u8, Timezone);

impl Hours {
    pub const fn new(from: u8, to_inclusive: u8, timezone: Timezone) -> Self {
        Self(from, to_inclusive, timezone)
    }

    pub(crate) const fn from(&self) -> u8 {
        self.0
    }

    #[allow(clippy::wrong_self_convention)]
    pub(crate) const fn to_inclusive(&self) -> u8 {
        self.1
    }

    pub(crate) fn translate(&self, language: Language) -> String {
        match language {
            // TODO: am/pm?
            Language::En => format!("{}-{}", self.from(), self.to_inclusive()),
            Language::Sv => format!("Kl {}-{}", self.from(), self.to_inclusive()),
        }
    }

    const fn start(&self) -> u8 {
        self.0
    }

    const fn end(&self) -> u8 {
        self.1
    }

    const fn tz(&self) -> chrono_tz::Tz {
        self.2.to_tz()
    }

    pub(crate) fn matches<Tz: chrono::TimeZone>(&self, timestamp: DateTime<Tz>) -> bool {
        let timestamp = timestamp.with_timezone(&self.tz());
        let hour = timestamp.hour() as u8;
        if self.start() <= self.end() {
            (self.start()..=self.end()).contains(&hour)
        } else {
            (self.start()..=23).contains(&hour) || (0..=self.end()).contains(&hour)
        }
    }
}

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

    use crate::{Stockholm, Utc};

    #[test]
    fn hours_matches_exact_start() {
        let hours = Hours::new(6, 22, Stockholm);
        let timestamp = Stockholm.dt(2025, 1, 15, 6, 0, 0);
        assert!(hours.matches(timestamp));
    }

    #[test]
    fn hours_matches_exact_end() {
        let hours = Hours::new(6, 22, Stockholm);
        let timestamp = Stockholm.dt(2025, 1, 15, 22, 59, 59);
        assert!(hours.matches(timestamp));
    }

    #[test]
    fn hours_matches_middle() {
        let hours = Hours::new(6, 22, Stockholm);
        let timestamp = Stockholm.dt(2025, 1, 15, 14, 30, 0);
        assert!(hours.matches(timestamp));
    }

    #[test]
    fn hours_does_not_match_before() {
        let hours = Hours::new(6, 22, Stockholm);
        let timestamp = Stockholm.dt(2025, 1, 15, 5, 59, 59);
        assert!(!hours.matches(timestamp));
    }

    #[test]
    fn hours_does_not_match_after() {
        let hours = Hours::new(6, 22, Stockholm);
        let timestamp = Stockholm.dt(2025, 1, 15, 23, 0, 0);
        assert!(!hours.matches(timestamp));
    }

    #[test]
    fn hours_matches_midnight_range() {
        let hours = Hours::new(0, 5, Stockholm);
        let timestamp = Stockholm.dt(2025, 1, 15, 3, 0, 0);
        assert!(hours.matches(timestamp));
    }

    #[test]
    fn hours_matches_all_day() {
        let hours = Hours::new(0, 23, Stockholm);
        let timestamp = Stockholm.dt(2025, 1, 15, 12, 0, 0);
        assert!(hours.matches(timestamp));
    }

    #[test]
    fn hours_matches_midnight_range_utc_near_end() {
        let hours = Hours::new(0, 5, Stockholm);
        let timestamp = Utc.dt(2025, 1, 15, 4, 59, 59);
        assert!(hours.matches(timestamp));
    }

    #[test]
    fn hours_matches_midnight_range_utc_at_end() {
        let hours = Hours::new(0, 5, Stockholm);
        let timestamp = Utc.dt(2025, 1, 15, 5, 0, 0);
        assert!(!hours.matches(timestamp))
    }

    // Stockholm to UTC tests - Winter time (UTC+1)
    #[test]
    fn hours_matches_morning_range_utc_winter() {
        let hours = Hours::new(6, 22, Stockholm);
        // 06:00 Stockholm = 05:00 UTC in winter
        let timestamp = Utc.dt(2025, 1, 15, 5, 0, 0);
        assert!(hours.matches(timestamp));
    }

    #[test]
    fn hours_does_not_match_before_start_utc_winter() {
        let hours = Hours::new(6, 22, Stockholm);
        // 05:59 Stockholm = 04:59 UTC in winter
        let timestamp = Utc.dt(2025, 1, 15, 4, 59, 59);
        assert!(!hours.matches(timestamp));
    }

    #[test]
    fn hours_matches_at_end_utc_winter() {
        let hours = Hours::new(6, 22, Stockholm);
        // 22:59 Stockholm = 21:59 UTC in winter
        let timestamp = Utc.dt(2025, 1, 15, 21, 59, 59);
        assert!(hours.matches(timestamp));
    }

    #[test]
    fn hours_does_not_match_after_end_utc_winter() {
        let hours = Hours::new(6, 22, Stockholm);
        // 23:00 Stockholm = 22:00 UTC in winter
        let timestamp = Utc.dt(2025, 1, 15, 22, 0, 0);
        assert!(!hours.matches(timestamp));
    }

    #[test]
    fn hours_matches_middle_utc_winter() {
        let hours = Hours::new(6, 22, Stockholm);
        // 14:00 Stockholm = 13:00 UTC in winter
        let timestamp = Utc.dt(2025, 1, 15, 13, 0, 0);
        assert!(hours.matches(timestamp));
    }

    // Stockholm to UTC tests - Summer time (UTC+2)
    #[test]
    fn hours_matches_morning_range_utc_summer() {
        let hours = Hours::new(6, 22, Stockholm);
        // 06:00 Stockholm = 04:00 UTC in summer
        let timestamp = Utc.dt(2025, 6, 15, 4, 0, 0);
        assert!(hours.matches(timestamp));
    }

    #[test]
    fn hours_does_not_match_before_start_utc_summer() {
        let hours = Hours::new(6, 22, Stockholm);
        // 05:59 Stockholm = 03:59 UTC in summer
        let timestamp = Utc.dt(2025, 6, 15, 3, 59, 59);
        assert!(!hours.matches(timestamp));
    }

    #[test]
    fn hours_matches_at_end_utc_summer() {
        let hours = Hours::new(6, 22, Stockholm);
        // 22:59 Stockholm = 20:59 UTC in summer
        let timestamp = Utc.dt(2025, 6, 15, 20, 59, 59);
        assert!(hours.matches(timestamp));
    }

    #[test]
    fn hours_does_not_match_after_end_utc_summer() {
        let hours = Hours::new(6, 22, Stockholm);
        // 23:00 Stockholm = 21:00 UTC in summer
        let timestamp = Utc.dt(2025, 6, 15, 21, 0, 0);
        assert!(!hours.matches(timestamp));
    }

    // Midnight wrap-around tests with UTC
    #[test]
    fn hours_matches_wraparound_late_evening_utc_winter() {
        let hours = Hours::new(22, 6, Stockholm);
        // 22:00 Stockholm = 21:00 UTC in winter
        let timestamp = Utc.dt(2025, 1, 15, 21, 0, 0);
        assert!(hours.matches(timestamp));
    }

    #[test]
    fn hours_matches_wraparound_midnight_utc_winter() {
        let hours = Hours::new(22, 6, Stockholm);
        // 00:00 Stockholm = 23:00 UTC (previous day) in winter
        let timestamp = Utc.dt(2025, 1, 14, 23, 0, 0);
        assert!(hours.matches(timestamp));
    }

    #[test]
    fn hours_matches_wraparound_early_morning_utc_winter() {
        let hours = Hours::new(22, 6, Stockholm);
        // 06:00 Stockholm = 05:00 UTC in winter
        let timestamp = Utc.dt(2025, 1, 15, 5, 0, 0);
        assert!(hours.matches(timestamp));
    }

    #[test]
    fn hours_does_not_match_wraparound_outside_range_utc_winter() {
        let hours = Hours::new(22, 6, Stockholm);
        // 07:00 Stockholm = 06:00 UTC in winter
        let timestamp = Utc.dt(2025, 1, 15, 6, 0, 0);
        assert!(!hours.matches(timestamp));
    }

    // Edge case: midnight start with UTC
    #[test]
    fn hours_matches_midnight_start_utc_winter() {
        let hours = Hours::new(0, 5, Stockholm);
        // 00:00 Stockholm = 23:00 UTC (previous day) in winter
        let timestamp = Utc.dt(2025, 1, 14, 23, 0, 0);
        assert!(hours.matches(timestamp));
    }

    #[test]
    fn hours_matches_midnight_start_early_hour_utc_winter() {
        let hours = Hours::new(0, 5, Stockholm);
        // 03:00 Stockholm = 02:00 UTC in winter
        let timestamp = Utc.dt(2025, 1, 15, 2, 0, 0);
        assert!(hours.matches(timestamp));
    }
}
