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

/// Systemd user timer generation for schedule profiles.
///
/// Generates `.timer` and `.service` unit files in ~/.config/systemd/user/
/// that call `temperlux schedule apply` at the appropriate times.
/// Works even when the GUI is not running.
use anyhow::Result;
use chrono::{Local, Timelike};
use std::fs;
use std::path::PathBuf;

use super::model::ScheduleTrigger;
use super::trigger::TriggerContext;

const UNIT_PREFIX: &str = "temperlux-schedule";

/// Check if any enabled profiles have sunrise/sunset triggers.
fn has_solar_triggers(profiles: &[crate::schedule::model::ScheduleProfile]) -> bool {
    profiles.iter().any(|p| {
        p.enabled && p.triggers.iter().any(|t| {
            matches!(t, ScheduleTrigger::Sunrise { .. } | ScheduleTrigger::Sunset { .. })
        })
    })
}

/// Generate systemd user timer + service units for all enabled profiles.
/// Removes any previously generated temperlux-schedule-* units first.
/// Also generates a daily regeneration timer so sunrise/sunset triggers
/// recalculate every day.
pub fn generate_timers(ctx: &TriggerContext) -> Result<Vec<String>> {
    let profiles = super::list_profiles()?;
    let unit_dir = systemd_unit_dir()?;

    // Clean old units
    clean_old_units(&unit_dir)?;

    let mut generated = Vec::new();

    // Check if any profiles use sunrise/sunset triggers (need daily regeneration)
    let has_solar = has_solar_triggers(&profiles);

    for profile in &profiles {
        if !profile.enabled || profile.triggers.is_empty() {
            continue;
        }

        for (i, trigger) in profile.triggers.iter().enumerate() {
            let unit_name = format!("{}-{}-{}", UNIT_PREFIX, &profile.id[..12], i);
            let on_calendar = trigger_to_calendar(trigger, ctx)?;

            // Service unit
            let service = format_service_unit(&unit_name, &profile.id);
            let service_path = unit_dir.join(format!("{}.service", unit_name));
            fs::write(&service_path, &service)?;

            // Timer unit
            let timer = format_timer_unit(&unit_name, &on_calendar);
            let timer_path = unit_dir.join(format!("{}.timer", unit_name));
            fs::write(&timer_path, &timer)?;

            generated.push(format!("{}.timer → {}", unit_name, on_calendar));
        }
    }

    // If any profiles use solar triggers, generate a daily regeneration timer
    // that recalculates sunrise/sunset times at 00:01 every day.
    if has_solar {
        let reload_unit_name = format!("{}-daily-reload", UNIT_PREFIX);
        let bin = temperlux_binary_path();

        // Service that regenerates all timers and reloads systemd
        let reload_service = format!(
            r#"[Unit]
Description=Temperlux: regenerate schedule timers for new day (solar trigger recalculation)

[Service]
Type=oneshot
ExecStart={} schedule generate-timers
ExecStartPost=/bin/sh -c 'systemctl --user daemon-reload && systemctl --user enable --now {}-*timer'
"#,
            bin, UNIT_PREFIX
        );
        fs::write(
            unit_dir.join(format!("{}.service", reload_unit_name)),
            &reload_service,
        )?;

        // Timer that fires daily at 00:01
        let reload_timer = r#"[Unit]
Description=Temperlux daily timer regeneration

[Timer]
OnCalendar=*-*-* 00:01:00
Persistent=true

[Install]
WantedBy=timers.target
"#.to_string();
        fs::write(
            unit_dir.join(format!("{}.timer", reload_unit_name)),
            &reload_timer,
        )?;

        generated.push(format!("{}.timer → daily solar recalculation at 00:01", reload_unit_name));
    }

    Ok(generated)
}

/// Remove all previously generated temperlux-schedule-* units.
fn clean_old_units(unit_dir: &std::path::Path) -> Result<()> {
    if !unit_dir.exists() {
        return Ok(());
    }
    for entry in fs::read_dir(unit_dir)? {
        let entry = entry?;
        let name = entry.file_name();
        let name_str = name.to_string_lossy();
        if name_str.starts_with(UNIT_PREFIX) {
            fs::remove_file(entry.path())?;
        }
    }
    Ok(())
}

/// Convert a trigger to a systemd OnCalendar expression.
fn trigger_to_calendar(trigger: &ScheduleTrigger, ctx: &TriggerContext) -> Result<String> {
    let now = Local::now();
    match trigger {
        ScheduleTrigger::Absolute { time } => {
            // "HH:MM" → "*-*-* HH:MM:00"
            let (h, m) = ScheduleTrigger::parse_absolute_time(time)
                .ok_or_else(|| anyhow::anyhow!("Invalid time: {}", time))?;
            Ok(format!("*-*-* {:02}:{:02}:00", h, m))
        }
        ScheduleTrigger::Sunrise { offset_minutes: _ } => {
            let resolved = super::trigger::resolve_trigger_time(trigger, ctx, now.date_naive())
                .ok_or_else(|| anyhow::anyhow!("Cannot resolve sunrise time"))?;
            // Daily timer at the resolved time (recalculated on timer reload)
            // We use a fixed time for today and note that sunrise changes daily.
            // For accurate daily sunrise, user should reload timers via cron.
            Ok(format!(
                "*-*-* {:02}:{:02}:00",
                resolved.hour(),
                resolved.minute()
            ))
        }
        ScheduleTrigger::Sunset { offset_minutes: _ } => {
            let resolved = super::trigger::resolve_trigger_time(trigger, ctx, now.date_naive())
                .ok_or_else(|| anyhow::anyhow!("Cannot resolve sunset time"))?;
            Ok(format!(
                "*-*-* {:02}:{:02}:00",
                resolved.hour(),
                resolved.minute()
            ))
        }
    }
}

fn format_service_unit(unit_name: &str, profile_id: &str) -> String {
    format!(
        r#"[Unit]
Description=Temperlux schedule: {}
After=graphical-session.target

[Service]
Type=oneshot
ExecStart={} schedule apply --profile {}
"#,
        unit_name,
        temperlux_binary_path(),
        profile_id,
    )
}

fn format_timer_unit(_unit_name: &str, on_calendar: &str) -> String {
    format!(
        r#"[Unit]
Description=Temperlux schedule timer

[Timer]
OnCalendar={}
Persistent=true

[Install]
WantedBy=timers.target
"#,
        on_calendar,
    )
}

fn temperlux_binary_path() -> String {
    // Always use the CLI binary, not the GUI binary.
    // When called from the GUI, current_exe() returns temperlux-gui which
    // doesn't support schedule subcommands.
    std::env::current_exe()
        .map(|p| {
            let path = p.display().to_string();
            // Replace -gui suffix (if any) with the CLI binary name
            if path.ends_with("-gui") {
                path.trim_end_matches("-gui").to_string()
            } else {
                path
            }
        })
        .unwrap_or_else(|_| "temperlux".to_string())
}

fn systemd_unit_dir() -> Result<PathBuf> {
    let dir = dirs::config_dir()
        .ok_or_else(|| anyhow::anyhow!("Could not find config directory"))?
        .join("systemd")
        .join("user");
    fs::create_dir_all(&dir)?;
    Ok(dir)
}

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

    #[test]
    fn test_trigger_to_calendar_absolute() {
        let ctx = TriggerContext::new(41.88, -87.63);
        let trigger = ScheduleTrigger::Absolute { time: "08:30".into() };
        let calendar = trigger_to_calendar(&trigger, &ctx).unwrap();
        assert_eq!(calendar, "*-*-* 08:30:00");
    }

    #[test]
    fn test_trigger_to_calendar_midnight() {
        let ctx = TriggerContext::new(41.88, -87.63);
        let trigger = ScheduleTrigger::Absolute { time: "00:00".into() };
        let calendar = trigger_to_calendar(&trigger, &ctx).unwrap();
        assert_eq!(calendar, "*-*-* 00:00:00");
    }

    #[test]
    fn test_trigger_to_calendar_sunrise() {
        let ctx = TriggerContext::new(41.88, -87.63);
        let trigger = ScheduleTrigger::Sunrise { offset_minutes: 0 };
        let calendar = trigger_to_calendar(&trigger, &ctx).unwrap();
        assert!(calendar.starts_with("*-*-* "));
        assert!(calendar.ends_with(":00"));
    }

    #[test]
    fn test_trigger_to_calendar_sunset_with_offset() {
        let ctx = TriggerContext::new(41.88, -87.63);
        let trigger = ScheduleTrigger::Sunset { offset_minutes: -40 };
        let calendar = trigger_to_calendar(&trigger, &ctx).unwrap();
        assert!(calendar.starts_with("*-*-* "));
        assert!(calendar.ends_with(":00"));
    }

    #[test]
    fn test_format_service_unit() {
        let svc = format_service_unit("temperlux-schedule-abc0", "full-id");
        assert!(svc.contains("[Unit]"));
        assert!(svc.contains("[Service]"));
        assert!(svc.contains("schedule apply --profile full-id"));
        assert!(svc.contains("Type=oneshot"));
    }

    #[test]
    fn test_format_timer_unit() {
        let timer = format_timer_unit("temperlux-schedule-abc0", "*-*-* 08:30:00");
        assert!(timer.contains("[Timer]"));
        assert!(timer.contains("OnCalendar=*-*-* 08:30:00"));
        assert!(timer.contains("Persistent=true"));
        assert!(timer.contains("WantedBy=timers.target"));
    }

    // --- Daily solar recalculation tests ---

    #[test]
    fn test_has_solar_triggers_absolute_only() {
        let mut p = ScheduleProfile::new("Abs".into());
        p.triggers.push(ScheduleTrigger::Absolute { time: "08:00".into() });
        assert!(!has_solar_triggers(&[p]));
    }

    #[test]
    fn test_has_solar_triggers_sunset() {
        let mut p = ScheduleProfile::new("Sunset".into());
        p.triggers.push(ScheduleTrigger::Sunset { offset_minutes: -40 });
        assert!(has_solar_triggers(&[p]));
    }

    #[test]
    fn test_has_solar_triggers_sunrise() {
        let mut p = ScheduleProfile::new("Sunrise".into());
        p.triggers.push(ScheduleTrigger::Sunrise { offset_minutes: 30 });
        assert!(has_solar_triggers(&[p]));
    }

    #[test]
    fn test_has_solar_triggers_mixed() {
        let mut p = ScheduleProfile::new("Mixed".into());
        p.triggers.push(ScheduleTrigger::Absolute { time: "08:00".into() });
        p.triggers.push(ScheduleTrigger::Sunset { offset_minutes: 0 });
        assert!(has_solar_triggers(&[p]));
    }

    #[test]
    fn test_has_solar_triggers_disabled_profile_ignored() {
        let mut p = ScheduleProfile::new("Disabled".into());
        p.enabled = false;
        p.triggers.push(ScheduleTrigger::Sunrise { offset_minutes: 0 });
        assert!(!has_solar_triggers(&[p]));
    }

    #[test]
    fn test_has_solar_triggers_empty() {
        assert!(!has_solar_triggers(&[]));
    }

    // --- Unit name collision tests ---

    #[test]
    fn test_unit_name_collision_prevention() {
        // Two real profile IDs that share the same 8-char prefix
        let id1 = "0000019e8fcd05290000";
        let id2 = "0000019ec7e741b20000";

        // Old (buggy): 8-char prefix — collision
        assert_eq!(&id1[..8], &id2[..8], "8-char prefix should collide");

        // New (fixed): 12-char prefix — distinct
        assert_ne!(&id1[..12], &id2[..12], "12-char prefix must be distinct");

        let unit1 = format!("{}-{}-{}", UNIT_PREFIX, &id1[..12], 0);
        let unit2 = format!("{}-{}-{}", UNIT_PREFIX, &id2[..12], 0);
        assert_ne!(unit1, unit2);
    }

    // --- Clean old units test ---

    #[test]
    fn test_clean_old_units_preserves_non_temperlux() {
        let dir = tempfile::tempdir().unwrap();
        let unit_dir = dir.path();

        fs::write(unit_dir.join("temperlux-schedule-abc.timer"), "test").unwrap();
        fs::write(unit_dir.join("temperlux-schedule-daily-reload.service"), "test").unwrap();
        fs::write(unit_dir.join("other-app.timer"), "test").unwrap();
        fs::write(unit_dir.join("my-service.service"), "test").unwrap();

        clean_old_units(unit_dir).unwrap();

        assert!(!unit_dir.join("temperlux-schedule-abc.timer").exists());
        assert!(!unit_dir.join("temperlux-schedule-daily-reload.service").exists());
        assert!(unit_dir.join("other-app.timer").exists());
        assert!(unit_dir.join("my-service.service").exists());
    }

    #[test]
    fn test_clean_old_units_empty_dir() {
        let dir = tempfile::tempdir().unwrap();
        clean_old_units(dir.path()).unwrap();
    }
}
