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

use serde::{Deserialize, Serialize};

/// A scheduled brightness profile — defines brightness values for displays,
/// with one or more triggers that determine when it fires.
///
/// A single profile can be reused at multiple times (e.g., 20min after sunrise
/// AND 20min before sunset) without duplicating the display settings.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScheduleProfile {
    /// Unique identifier.
    #[serde(default = "new_uuid")]
    pub id: String,
    /// User-visible name.
    pub name: String,
    /// Monitor brightness entries (display key → brightness %).
    #[serde(default)]
    pub entries: Vec<ScheduleEntry>,
    /// When to trigger — multiple triggers per profile are supported.
    #[serde(default)]
    pub triggers: Vec<ScheduleTrigger>,
    /// Whether this profile is active.
    #[serde(default = "default_true")]
    pub enabled: bool,
}

fn default_true() -> bool {
    true
}

fn new_uuid() -> String {
    // Simple random-ish ID from timestamp + counter (no external dep needed)
    use std::sync::atomic::{AtomicU64, Ordering};
    static COUNTER: AtomicU64 = AtomicU64::new(0);
    let ts = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64;
    let c = COUNTER.fetch_add(1, Ordering::Relaxed);
    format!("{:016x}{:04x}", ts, c % 65536)
}

impl Default for ScheduleProfile {
    fn default() -> Self {
        Self {
            id: new_uuid(),
            name: String::new(),
            entries: Vec::new(),
            triggers: Vec::new(),
            enabled: true,
        }
    }
}

/// Brightness setting for a single display within a schedule.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScheduleEntry {
    /// Display backend key (e.g. "ddc:1", "backlight:intel_backlight").
    pub display_key: String,
    /// Brightness percentage (0-100).
    pub brightness: f64,
}

/// When a schedule should trigger.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ScheduleTrigger {
    /// Specific time of day.
    Absolute {
        /// Time as "HH:MM" (24-hour, local timezone).
        time: String,
    },
    /// Relative to sunrise.
    Sunrise {
        /// Offset in minutes from sunrise. Negative = before, positive = after.
        #[serde(default)]
        offset_minutes: i32,
    },
    /// Relative to sunset.
    Sunset {
        /// Offset in minutes from sunset. Negative = before, positive = after.
        #[serde(default)]
        offset_minutes: i32,
    },
}

impl Default for ScheduleTrigger {
    fn default() -> Self {
        ScheduleTrigger::Absolute {
            time: "08:00".to_string(),
        }
    }
}

impl ScheduleTrigger {
    /// Parse absolute time string "HH:MM" into (hours, minutes).
    pub fn parse_absolute_time(time: &str) -> Option<(u32, u32)> {
        let parts: Vec<&str> = time.split(':').collect();
        if parts.len() != 2 {
            return None;
        }
        let h: u32 = parts[0].parse().ok()?;
        let m: u32 = parts[1].parse().ok()?;
        if h > 23 || m > 59 {
            return None;
        }
        Some((h, m))
    }

    /// Get a human-readable description of this trigger (24h format).
    pub fn description(&self) -> String {
        self.description_with_format(true)
    }

    /// Get a human-readable description with time format preference.
    pub fn description_with_format(&self, use_24h: bool) -> String {
        use crate::i18n::{tr, tr_args};
        match self {
            ScheduleTrigger::Absolute { time } => {
                if let Some((h, m)) = Self::parse_absolute_time(time) {
                    if use_24h {
                        format!("{} {:02}:{:02}", tr("triggers.at_prefix"), h, m)
                    } else {
                        let (h12, suffix) = match h {
                            0 => (12, "AM"),
                            1..=11 => (h, "AM"),
                            12 => (12, "PM"),
                            _ => (h - 12, "PM"),
                        };
                        format!("{} {}:{:02} {}", tr("triggers.at_prefix"), h12, m, suffix)
                    }
                } else {
                    format!("{} {}", tr("triggers.at_prefix"), time)
                }
            }
            ScheduleTrigger::Sunrise { offset_minutes } => {
                if *offset_minutes == 0 {
                    tr("triggers.at_sunrise")
                } else if *offset_minutes > 0 {
                    tr_args("triggers.min_after_sunrise", &[("count", &offset_minutes.to_string())])
                } else {
                    tr_args("triggers.min_before_sunrise", &[("count", &(-offset_minutes).to_string())])
                }
            }
            ScheduleTrigger::Sunset { offset_minutes } => {
                if *offset_minutes == 0 {
                    tr("triggers.at_sunset")
                } else if *offset_minutes > 0 {
                    tr_args("triggers.min_after_sunset", &[("count", &offset_minutes.to_string())])
                } else {
                    tr_args("triggers.min_before_sunset", &[("count", &(-offset_minutes).to_string())])
                }
            }
        }
    }
}

impl ScheduleProfile {
    pub fn new(name: String) -> Self {
        Self {
            name,
            ..Default::default()
        }
    }

    /// Get the entry for a specific display key.
    pub fn entry_for(&self, display_key: &str) -> Option<&ScheduleEntry> {
        self.entries.iter().find(|e| e.display_key == display_key)
    }

    /// Set or update the brightness for a display key.
    pub fn set_entry(&mut self, display_key: String, brightness: f64) {
        let brightness = brightness.clamp(0.0, 100.0);
        if let Some(entry) = self.entries.iter_mut().find(|e| e.display_key == display_key) {
            entry.brightness = brightness;
        } else {
            self.entries.push(ScheduleEntry {
                display_key,
                brightness,
            });
        }
    }

    /// Remove an entry by display key.
    pub fn remove_entry(&mut self, display_key: &str) {
        self.entries.retain(|e| e.display_key != display_key);
    }

    /// Add a trigger to this profile.
    pub fn add_trigger(&mut self, trigger: ScheduleTrigger) {
        self.triggers.push(trigger);
    }

    /// Remove a trigger by index.
    pub fn remove_trigger(&mut self, index: usize) {
        if index < self.triggers.len() {
            self.triggers.remove(index);
        }
    }

    /// Get a summary description of all triggers (24h format).
    pub fn trigger_summary(&self) -> String {
        self.trigger_summary_with_format(true)
    }

    /// Get a summary description of all triggers with time format preference.
    pub fn trigger_summary_with_format(&self, use_24h: bool) -> String {
        if self.triggers.is_empty() {
            crate::i18n::tr("triggers.no_triggers")
        } else {
            self.triggers
                .iter()
                .map(|t| t.description_with_format(use_24h))
                .collect::<Vec<_>>()
                .join(", ")
        }
    }
}

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

    fn ensure_en_locale() {
        rust_i18n::set_locale("en");
    }

    #[test]
    fn test_parse_absolute_time_valid() {
        assert_eq!(ScheduleTrigger::parse_absolute_time("08:30"), Some((8, 30)));
        assert_eq!(ScheduleTrigger::parse_absolute_time("23:59"), Some((23, 59)));
        assert_eq!(ScheduleTrigger::parse_absolute_time("00:00"), Some((0, 0)));
    }

    #[test]
    fn test_parse_absolute_time_invalid() {
        assert_eq!(ScheduleTrigger::parse_absolute_time("24:00"), None);
        assert_eq!(ScheduleTrigger::parse_absolute_time("12:60"), None);
        assert_eq!(ScheduleTrigger::parse_absolute_time("abc"), None);
        assert_eq!(ScheduleTrigger::parse_absolute_time("8:30:00"), None);
    }

    #[test]
    fn test_trigger_description() {
        ensure_en_locale();
        let t = ScheduleTrigger::Absolute { time: "08:30".into() };
        assert_eq!(t.description(), "at 08:30");

        let t = ScheduleTrigger::Sunrise { offset_minutes: 0 };
        assert_eq!(t.description(), "at sunrise");

        let t = ScheduleTrigger::Sunrise { offset_minutes: -30 };
        assert_eq!(t.description(), "30min before sunrise");

        let t = ScheduleTrigger::Sunset { offset_minutes: 60 };
        assert_eq!(t.description(), "60min after sunset");
    }

    #[test]
    fn test_set_entry_add_and_update() {
        let mut p = ScheduleProfile::new("Test".into());
        p.set_entry("ddc:1".into(), 50.0);
        assert_eq!(p.entries.len(), 1);
        assert_eq!(p.entries[0].brightness, 50.0);

        p.set_entry("ddc:1".into(), 75.0);
        assert_eq!(p.entries.len(), 1);
        assert_eq!(p.entries[0].brightness, 75.0);
    }

    #[test]
    fn test_remove_entry() {
        let mut p = ScheduleProfile::new("Test".into());
        p.set_entry("ddc:1".into(), 50.0);
        p.set_entry("ddc:2".into(), 80.0);
        p.remove_entry("ddc:1");
        assert_eq!(p.entries.len(), 1);
        assert_eq!(p.entries[0].display_key, "ddc:2");
    }

    #[test]
    fn test_brightness_clamped() {
        let mut p = ScheduleProfile::new("Test".into());
        p.set_entry("ddc:1".into(), 150.0);
        assert_eq!(p.entries[0].brightness, 100.0);

        p.set_entry("ddc:1".into(), -10.0);
        assert_eq!(p.entries[0].brightness, 0.0);
    }

    #[test]
    fn test_serde_roundtrip() {
        let mut p = ScheduleProfile::new("Daytime".into());
        p.set_entry("ddc:1".into(), 80.0);
        p.triggers.push(ScheduleTrigger::Sunrise { offset_minutes: 20 });
        p.triggers.push(ScheduleTrigger::Sunset { offset_minutes: -20 });

        let json = serde_json::to_string(&p).unwrap();
        let p2: ScheduleProfile = serde_json::from_str(&json).unwrap();
        assert_eq!(p2.name, "Daytime");
        assert_eq!(p2.entries.len(), 1);
        assert_eq!(p2.triggers.len(), 2);
        assert_eq!(p2.triggers[0], ScheduleTrigger::Sunrise { offset_minutes: 20 });
        assert_eq!(p2.triggers[1], ScheduleTrigger::Sunset { offset_minutes: -20 });
    }

    #[test]
    fn test_trigger_summary_empty() {
        ensure_en_locale();
        let p = ScheduleProfile::new("Test".into());
        assert_eq!(p.trigger_summary(), "no triggers");
    }

    #[test]
    fn test_trigger_summary_multiple() {
        ensure_en_locale();
        let mut p = ScheduleProfile::new("Test".into());
        p.add_trigger(ScheduleTrigger::Absolute { time: "08:00".into() });
        p.add_trigger(ScheduleTrigger::Sunset { offset_minutes: -30 });
        assert_eq!(p.trigger_summary(), "at 08:00, 30min before sunset");
    }

    #[test]
    fn test_add_and_remove_trigger() {
        let mut p = ScheduleProfile::new("Test".into());
        p.add_trigger(ScheduleTrigger::Absolute { time: "08:00".into() });
        p.add_trigger(ScheduleTrigger::Sunrise { offset_minutes: 0 });
        p.add_trigger(ScheduleTrigger::Sunset { offset_minutes: 60 });
        assert_eq!(p.triggers.len(), 3);

        p.remove_trigger(1); // Remove sunrise
        assert_eq!(p.triggers.len(), 2);
        assert_eq!(p.triggers[0], ScheduleTrigger::Absolute { time: "08:00".into() });
        assert_eq!(p.triggers[1], ScheduleTrigger::Sunset { offset_minutes: 60 });

        // Remove out-of-bounds index — should be no-op
        p.remove_trigger(99);
        assert_eq!(p.triggers.len(), 2);
    }

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

        assert!(p.entry_for("ddc:1").is_some());
        assert_eq!(p.entry_for("ddc:1").unwrap().brightness, 50.0);
        assert!(p.entry_for("ddc:99").is_none());
    }

    #[test]
    fn test_remove_entry_nonexistent() {
        let mut p = ScheduleProfile::new("Test".into());
        p.set_entry("ddc:1".into(), 50.0);
        p.remove_entry("ddc:99"); // Should not panic
        assert_eq!(p.entries.len(), 1);
    }

    #[test]
    fn test_profile_default_enabled() {
        let p = ScheduleProfile::default();
        assert!(p.enabled);
        assert!(p.entries.is_empty());
        assert!(p.triggers.is_empty());
    }

    #[test]
    fn test_profile_unique_ids() {
        let p1 = ScheduleProfile::new("A".into());
        let p2 = ScheduleProfile::new("B".into());
        assert_ne!(p1.id, p2.id);
    }

    #[test]
    fn test_serde_backward_compat_empty_triggers() {
        // Simulate loading a profile with no triggers field
        let json = r#"{"id":"test123","name":"Old","entries":[{"display_key":"ddc:1","brightness":50.0}],"enabled":true}"#;
        let p: ScheduleProfile = serde_json::from_str(json).unwrap();
        assert_eq!(p.name, "Old");
        assert!(p.triggers.is_empty());
    }

    #[test]
    fn test_serde_disabled_profile() {
        let mut p = ScheduleProfile::new("Night".into());
        p.enabled = false;
        p.add_trigger(ScheduleTrigger::Absolute { time: "22:00".into() });
        let json = serde_json::to_string(&p).unwrap();
        let p2: ScheduleProfile = serde_json::from_str(&json).unwrap();
        assert!(!p2.enabled);
    }

    #[test]
    fn test_parse_time_edge_cases() {
        // Single digit hour
        assert_eq!(ScheduleTrigger::parse_absolute_time("8:30"), Some((8, 30)));
        // Empty
        assert_eq!(ScheduleTrigger::parse_absolute_time(""), None);
        // Just numbers
        assert_eq!(ScheduleTrigger::parse_absolute_time("1234"), None);
        // Negative
        assert_eq!(ScheduleTrigger::parse_absolute_time("-1:30"), None);
    }

    #[test]
    fn test_sunset_description_zero_offset() {
        ensure_en_locale();
        let t = ScheduleTrigger::Sunset { offset_minutes: 0 };
        assert_eq!(t.description(), "at sunset");
    }
}
