// SPDX-License-Identifier: GPL-3.0-or-later

/// Trigger matching — determines if a schedule should fire at the current time.
use chrono::{Local, NaiveDate, NaiveTime, Timelike};

use super::model::ScheduleTrigger;
use super::sunrise;

/// Configuration needed for trigger evaluation.
#[derive(Debug, Clone)]
pub struct TriggerContext {
    /// Latitude in degrees (for sunrise/sunset calculation).
    pub latitude: f64,
    /// Longitude in degrees (for sunrise/sunset calculation).
    pub longitude: f64,
}

impl TriggerContext {
    pub fn new(latitude: f64, longitude: f64) -> Self {
        Self {
            latitude,
            longitude,
        }
    }
}

impl Default for TriggerContext {
    fn default() -> Self {
        // Default to Chicago (41.88°N, 87.63°W) — will be overridden by settings
        Self {
            latitude: 41.88,
            longitude: -87.63,
        }
    }
}

/// Result of checking whether a trigger matches the current time.
#[derive(Debug, Clone, PartialEq)]
pub enum TriggerMatch {
    /// The trigger matches now — should apply the profile.
    Match,
    /// The trigger does not match. Contains the seconds until the next match.
    NoMatch { next_in_secs: u64 },
}

/// Check if a trigger should fire at the given local time.
///
/// Uses a ±60 second window to account for polling intervals.
/// Returns whether the trigger matches and, if not, how long until next match.
pub fn check_trigger(
    trigger: &ScheduleTrigger,
    ctx: &TriggerContext,
    now: &chrono::DateTime<Local>,
) -> TriggerMatch {
    let target_time = match resolve_trigger_time(trigger, ctx, now.date_naive()) {
        Some(t) => t,
        None => {
            // Can't resolve (e.g. polar region, no sunrise/sunset)
            return TriggerMatch::NoMatch { next_in_secs: 3600 };
        }
    };

    let now_time = now.time();
    let now_secs = time_to_secs(now_time);
    let target_secs = time_to_secs(target_time);

    // Match if we're within the poll window AFTER the trigger time (0..+90s).
    // This prevents early firing and keeps the window tight for a 60s poll interval.
    if now_secs >= target_secs && (now_secs - target_secs) <= 90 {
        return TriggerMatch::Match;
    }

    // Calculate seconds until next occurrence
    let next_in_secs = if now_secs < target_secs {
        target_secs - now_secs
    } else {
        // Next occurrence is tomorrow
        86400 - now_secs + target_secs
    };

    TriggerMatch::NoMatch { next_in_secs }
}

/// Resolve a trigger to a concrete time for a given date.
pub fn resolve_trigger_time(
    trigger: &ScheduleTrigger,
    ctx: &TriggerContext,
    date: NaiveDate,
) -> Option<NaiveTime> {
    match trigger {
        ScheduleTrigger::Absolute { time } => {
            ScheduleTrigger::parse_absolute_time(time).map(|(h, m)| {
                NaiveTime::from_hms_opt(h, m, 0).unwrap()
            })
        }
        ScheduleTrigger::Sunrise { offset_minutes } => {
            let solar = sunrise::calculate(date, ctx.latitude, ctx.longitude)?;
            let base_secs = time_to_secs(solar.sunrise);
            let adjusted = apply_offset(base_secs, *offset_minutes);
            Some(secs_to_time(adjusted))
        }
        ScheduleTrigger::Sunset { offset_minutes } => {
            let solar = sunrise::calculate(date, ctx.latitude, ctx.longitude)?;
            let base_secs = time_to_secs(solar.sunset);
            let adjusted = apply_offset(base_secs, *offset_minutes);
            Some(secs_to_time(adjusted))
        }
    }
}

fn time_to_secs(t: NaiveTime) -> u64 {
    t.num_seconds_from_midnight() as u64
}

fn secs_to_time(s: u64) -> NaiveTime {
    let s = s % 86400; // wrap around midnight
    let h = (s / 3600) as u32;
    let m = ((s % 3600) / 60) as u32;
    let sec = (s % 60) as u32;
    NaiveTime::from_hms_opt(h, m, sec).unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap())
}

fn apply_offset(base_secs: u64, offset_minutes: i32) -> u64 {
    let offset_secs = (offset_minutes as i64) * 60;
    let result = base_secs as i64 + offset_secs;
    if result < 0 {
        (result + 86400) as u64
    } else {
        result as u64
    }
}

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

    fn test_ctx() -> TriggerContext {
        TriggerContext::new(41.88, -87.63)
    }

    #[test]
    fn test_absolute_trigger_matches() {
        let now = Local.with_ymd_and_hms(2026, 6, 15, 8, 30, 0).unwrap();
        let trigger = ScheduleTrigger::Absolute { time: "08:30".into() };
        let result = check_trigger(&trigger, &test_ctx(), &now);
        assert_eq!(result, TriggerMatch::Match);
    }

    #[test]
    fn test_absolute_trigger_no_match() {
        let now = Local.with_ymd_and_hms(2026, 6, 15, 8, 0, 0).unwrap();
        let trigger = ScheduleTrigger::Absolute { time: "08:30".into() };
        let result = check_trigger(&trigger, &test_ctx(), &now);
        assert!(matches!(result, TriggerMatch::NoMatch { .. }));
    }

    #[test]
    fn test_absolute_trigger_near_match() {
        // Within ±60 second window
        let now = Local.with_ymd_and_hms(2026, 6, 15, 8, 30, 45).unwrap();
        let trigger = ScheduleTrigger::Absolute { time: "08:30".into() };
        let result = check_trigger(&trigger, &test_ctx(), &now);
        assert_eq!(result, TriggerMatch::Match);
    }

    #[test]
    fn test_sunrise_trigger_resolves() {
        let date = NaiveDate::from_ymd_opt(2026, 6, 15).unwrap();
        let trigger = ScheduleTrigger::Sunrise { offset_minutes: 0 };
        let time = resolve_trigger_time(&trigger, &test_ctx(), date);
        assert!(time.is_some());
        // Should be early morning
        let t = time.unwrap();
        assert!(t.hour() < 8, "Sunrise should be before 8 AM, got {}", t);
    }

    #[test]
    fn test_sunset_trigger_with_offset() {
        let date = NaiveDate::from_ymd_opt(2026, 6, 15).unwrap();
        let trigger = ScheduleTrigger::Sunset { offset_minutes: 30 };
        let time = resolve_trigger_time(&trigger, &test_ctx(), date);
        assert!(time.is_some());
    }

    #[test]
    fn test_offset_before_sunrise() {
        let date = NaiveDate::from_ymd_opt(2026, 6, 15).unwrap();

        let t0 = resolve_trigger_time(
            &ScheduleTrigger::Sunrise { offset_minutes: 0 },
            &test_ctx(),
            date,
        ).unwrap();

        let t_minus30 = resolve_trigger_time(
            &ScheduleTrigger::Sunrise { offset_minutes: -30 },
            &test_ctx(),
            date,
        ).unwrap();

        // 30 minutes before should be earlier
        assert!(t_minus30 < t0, "{} should be before {}", t_minus30, t0);
    }

    #[test]
    fn test_next_in_secs_calculation() {
        let now = Local.with_ymd_and_hms(2026, 6, 15, 8, 0, 0).unwrap();
        let trigger = ScheduleTrigger::Absolute { time: "08:30".into() };
        let result = check_trigger(&trigger, &test_ctx(), &now);
        if let TriggerMatch::NoMatch { next_in_secs } = result {
            // 30 minutes = 1800 seconds
            assert!(
                (next_in_secs as i64 - 1800).abs() < 5,
                "Expected ~1800s, got {}",
                next_in_secs
            );
        } else {
            panic!("Expected NoMatch");
        }
    }

    #[test]
    fn test_next_in_secs_wraps_midnight() {
        // Trigger at 06:00, current time 23:00 → next is tomorrow at 06:00
        let now = Local.with_ymd_and_hms(2026, 6, 15, 23, 0, 0).unwrap();
        let trigger = ScheduleTrigger::Absolute { time: "06:00".into() };
        let result = check_trigger(&trigger, &test_ctx(), &now);
        if let TriggerMatch::NoMatch { next_in_secs } = result {
            // 7 hours = 25200 seconds
            assert!(
                (next_in_secs as i64 - 25200).abs() < 5,
                "Expected ~25200s, got {}",
                next_in_secs
            );
        } else {
            panic!("Expected NoMatch");
        }
    }

    #[test]
    fn test_match_exactly_at_target_time() {
        let now = Local.with_ymd_and_hms(2026, 6, 15, 12, 0, 0).unwrap();
        let trigger = ScheduleTrigger::Absolute { time: "12:00".into() };
        let result = check_trigger(&trigger, &test_ctx(), &now);
        assert_eq!(result, TriggerMatch::Match);
    }

    #[test]
    fn test_no_match_before_trigger_time() {
        // 45 seconds before trigger → should NOT match (only forward window)
        let now = Local.with_ymd_and_hms(2026, 6, 15, 8, 29, 15).unwrap();
        let trigger = ScheduleTrigger::Absolute { time: "08:30".into() };
        let result = check_trigger(&trigger, &test_ctx(), &now);
        assert!(matches!(result, TriggerMatch::NoMatch { .. }));
    }

    #[test]
    fn test_match_within_90s_after() {
        // 45 seconds after trigger → should match
        let now = Local.with_ymd_and_hms(2026, 6, 15, 8, 30, 45).unwrap();
        let trigger = ScheduleTrigger::Absolute { time: "08:30".into() };
        let result = check_trigger(&trigger, &test_ctx(), &now);
        assert_eq!(result, TriggerMatch::Match);
    }

    #[test]
    fn test_no_match_61_seconds_before() {
        let now = Local.with_ymd_and_hms(2026, 6, 15, 8, 28, 59).unwrap();
        let trigger = ScheduleTrigger::Absolute { time: "08:30".into() };
        let result = check_trigger(&trigger, &test_ctx(), &now);
        assert!(matches!(result, TriggerMatch::NoMatch { .. }));
    }

    #[test]
    fn test_sunrise_matching_near_time() {
        // Resolve sunrise, then check matching at that exact time
        let ctx = test_ctx();
        let now = Local.with_ymd_and_hms(2026, 6, 15, 12, 0, 0).unwrap();
        let sunrise_time = resolve_trigger_time(
            &ScheduleTrigger::Sunrise { offset_minutes: 0 },
            &ctx,
            now.date_naive(),
        ).unwrap();

        // Create a datetime at sunrise (including seconds for exact match)
        let sunrise_dt = Local.with_ymd_and_hms(
            2026, 6, 15,
            sunrise_time.hour(), sunrise_time.minute(), sunrise_time.second(),
        ).unwrap();
        let result = check_trigger(
            &ScheduleTrigger::Sunrise { offset_minutes: 0 },
            &ctx,
            &sunrise_dt,
        );
        assert_eq!(result, TriggerMatch::Match);
    }

    #[test]
    fn test_offset_minutes_wrap() {
        // 30 minutes before midnight → should wrap to 23:30
        // Test that apply_offset handles wrapping
        let result = super::apply_offset(600, -30); // 00:10 - 30min = -1200 → wrap
        let time = super::secs_to_time(result);
        // Should be 23:40 (previous day)
        assert_eq!(time.hour(), 23);
        assert_eq!(time.minute(), 40);
    }
}
