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

/// Schedule profiles — automatic brightness adjustment based on time of day.
///
/// This module is a pure library with no GUI dependencies.
/// Both CLI and GUI use this as the single source of truth for schedule logic.
pub mod model;
pub mod persistence;
pub mod sunrise;
pub mod systemd;
pub mod trigger;

use anyhow::Result;
use chrono::Local;

use model::ScheduleProfile;
use persistence as store;
use trigger::TriggerContext;

/// Check all schedules and return profiles that should fire now.
pub fn check_due_profiles(ctx: &TriggerContext) -> Result<Vec<ScheduleProfile>> {
    let profiles = store::load()?;
    let now = Local::now();
    let mut due = Vec::new();

    for profile in &profiles {
        if !profile.enabled {
            continue;
        }
        // Check if any of the profile's triggers match now
        for t in &profile.triggers {
            let match_result = trigger::check_trigger(t, ctx, &now);
            if let trigger::TriggerMatch::Match = match_result {
                due.push(profile.clone());
                break; // Don't add the same profile twice
            }
        }
    }

    Ok(due)
}

/// Apply a schedule profile — sets brightness for each entry.
///
/// This is the shared implementation used by both CLI and GUI.
/// It returns the actions taken so callers can log/display them.
pub fn apply_profile(
    profile: &ScheduleProfile,
    set_fn: &dyn Fn(&str, f64) -> Result<()>,
) -> Result<Vec<(String, f64)>> {
    let mut applied = Vec::new();
    for entry in &profile.entries {
        set_fn(&entry.display_key, entry.brightness)?;
        applied.push((entry.display_key.clone(), entry.brightness));
    }
    Ok(applied)
}

/// Create a new schedule profile with a generated ID.
pub fn create_profile(name: String) -> ScheduleProfile {
    ScheduleProfile::new(name)
}

/// Delete a profile by ID. Returns true if found and deleted.
pub fn delete_profile(id: &str) -> Result<bool> {
    store::delete(id)
}

/// List all profiles.
pub fn list_profiles() -> Result<Vec<ScheduleProfile>> {
    store::load()
}

/// Get a profile by ID.
pub fn get_profile(id: &str) -> Result<Option<ScheduleProfile>> {
    store::get_by_id(id)
}

/// Save a profile (adds if new, updates if existing ID matches).
pub fn save_profile(profile: ScheduleProfile) -> Result<()> {
    let existing = store::get_by_id(&profile.id)?;
    match existing {
        Some(_) => {
            store::update(profile)?;
        }
        None => {
            store::add(profile)?;
        }
    }
    Ok(())
}

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

    #[test]
    fn test_apply_profile() {
        let mut profile = ScheduleProfile::new("Test".into());
        profile.entries.push(ScheduleEntry {
            display_key: "ddc:1".into(),
            brightness: 50.0,
        });
        profile.entries.push(ScheduleEntry {
            display_key: "ddc:2".into(),
            brightness: 80.0,
        });

        let calls = std::cell::RefCell::new(Vec::new());
        let result = apply_profile(&profile, &|key, val| {
            calls.borrow_mut().push((key.to_string(), val));
            Ok(())
        }).unwrap();

        assert_eq!(result.len(), 2);
        let calls = calls.into_inner();
        assert_eq!(calls[0], ("ddc:1".to_string(), 50.0));
        assert_eq!(calls[1], ("ddc:2".to_string(), 80.0));
    }

    #[test]
    fn test_create_profile_has_id() {
        let p = create_profile("Morning".into());
        assert!(!p.id.is_empty());
        assert_eq!(p.name, "Morning");
        assert!(p.entries.is_empty());
        assert!(p.enabled);
    }

    #[test]
    fn test_apply_profile_empty_entries() {
        let profile = ScheduleProfile::new("Empty".into());
        let result = apply_profile(&profile, &|_key, _val| {
            panic!("Should not be called for empty profile");
        }).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_apply_profile_reports_errors() {
        let mut profile = ScheduleProfile::new("Test".into());
        profile.set_entry("ddc:1".into(), 50.0);

        let result = apply_profile(&profile, &|_key, _val| {
            Err(anyhow::anyhow!("Display not found"))
        });
        assert!(result.is_err());
    }
}
