use crate::config::Config;
use crate::elements::Element;
use crate::meta::MetaLocal;
use crate::store::Store;
use crate::time_parse;
use anyhow::Result;
use chrono::{Duration, Local, NaiveTime, Timelike};
use colored::Colorize;

enum NotifyEvent {
    Reminder { epoch_ref: String, body: String },
    Tasks { count: usize, lines: Vec<String> },
}

const MAX_TASK_LINES: usize = 10;

impl NotifyEvent {
    fn title(&self) -> &str {
        match self {
            NotifyEvent::Reminder { .. } => "mps reminder",
            NotifyEvent::Tasks { .. } => "mps · open tasks",
        }
    }

    fn body(&self) -> String {
        match self {
            NotifyEvent::Reminder { body, .. } => body.trim().to_string(),
            NotifyEvent::Tasks { count, lines } => {
                let shown = lines.len().min(MAX_TASK_LINES);
                let mut out = format!("{} open task(s)\n{}", count, lines[..shown].join("\n"));
                if *count > shown {
                    out.push_str(&format!("\n… and {} more", count - shown));
                }
                out
            }
        }
    }
}

/// Returns true if a reminder set for `reminder_time` should fire right now.
///
/// A reminder fires when the absolute difference between `now_time` and
/// `reminder_time` is within `window_mins` minutes, AND it hasn't been sent
/// within `cooldown_secs` already (unless `force` is set).
pub(crate) fn reminder_is_due(
    now_time: NaiveTime,
    reminder_time: NaiveTime,
    window_mins: u64,
    epoch_ref: &str,
    local: &MetaLocal,
    force: bool,
    cooldown_secs: i64,
) -> bool {
    let delta = (now_time.num_seconds_from_midnight() as i64
        - reminder_time.num_seconds_from_midnight() as i64)
        .abs();
    if delta > (window_mins * 60) as i64 {
        return false;
    }
    !(!force && local.was_notified(epoch_ref, cooldown_secs))
}

/// Returns true if the open-task briefing should be sent now.
///
/// The briefing fires when `now_time` is within `window_mins` of `briefing_time`
/// AND it hasn't been sent today yet (unless `force` is set).
pub(crate) fn briefing_is_due(
    now_time: NaiveTime,
    briefing_time: NaiveTime,
    window_mins: u64,
    local: &MetaLocal,
    force: bool,
) -> bool {
    let in_window = (now_time.num_seconds_from_midnight() as i64
        - briefing_time.num_seconds_from_midnight() as i64)
        .abs()
        <= (window_mins * 60) as i64;
    in_window && (force || !local.task_briefing_done_today())
}

pub fn run(
    config: &Config,
    dry_run: bool,
    window_override: Option<u64>,
    force: bool,
) -> Result<()> {
    if !config.notify.enabled {
        if dry_run {
            println!("(notifications disabled in config)");
        }
        return Ok(());
    }

    let mut local = MetaLocal::load(&config.storage_dir);
    let store = Store::new(&config.storage_dir);
    let window_mins = window_override.unwrap_or(config.notify.window_minutes);
    let now = Local::now();
    let now_time = now.time();
    let today = now.date_naive();

    let mut queued: Vec<NotifyEvent> = Vec::new();

    // ── Reminders (today's file) ─────────────────────────────────────────────
    let elements = store.parse_date(today).unwrap_or_default();
    for (epoch_ref, el) in &elements {
        let data = match el {
            Element::Reminder { data, .. } => data,
            _ => continue,
        };
        let at_str = match &data.at {
            Some(s) => s.as_str(),
            None => continue,
        };
        let reminder_time = match time_parse::parse_time(at_str) {
            Ok(t) => t,
            Err(_) => {
                eprintln!(
                    "  {} cannot parse reminder time '{}' — skipping",
                    "warn:".yellow(),
                    at_str
                );
                continue;
            }
        };

        let cooldown_secs = (config.notify.task_cooldown_minutes * 60) as i64;
        if !reminder_is_due(now_time, reminder_time, window_mins, epoch_ref, &local, force, cooldown_secs) {
            continue;
        }

        queued.push(NotifyEvent::Reminder {
            epoch_ref: epoch_ref.clone(),
            body: el.body_str().trim().to_string(),
        });
    }

    // ── Open-task briefing (morning, once per day) ───────────────────────────
    if config.notify.notify_open_tasks {
        let should_check = if let Some(ref at_str) = config.notify.task_notify_at {
            match time_parse::parse_time(at_str) {
                Ok(briefing_time) => briefing_is_due(now_time, briefing_time, window_mins, &local, force),
                Err(_) => {
                    eprintln!(
                        "  {} cannot parse task_notify_at '{}' — skipping task briefing",
                        "warn:".yellow(),
                        at_str
                    );
                    false
                }
            }
        } else {
            false // briefing-style requires task_notify_at to be set
        };

        if should_check {
            let tag_filter = &config.notify.open_task_tags;
            let overdue_days = config.notify.overdue_days;
            let mut open_lines: Vec<String> = Vec::new();

            for day_offset in 0..=(overdue_days as i64) {
                let scan_date = today - Duration::days(day_offset);
                let day_els = store.parse_date(scan_date).unwrap_or_default();
                for el in day_els.values() {
                    if let Element::Task { data, .. } = el {
                        if data.is_done() {
                            continue;
                        }
                        if !tag_filter.is_empty()
                            && !el.tags().iter().any(|t| tag_filter.contains(t))
                        {
                            continue;
                        }
                        let prefix = if day_offset == 0 {
                            String::new()
                        } else {
                            format!("[{} days ago] ", day_offset)
                        };
                        open_lines.push(format!("• {}{}", prefix, el.body_str().trim()));
                    }
                }
            }

            if !open_lines.is_empty() {
                queued.push(NotifyEvent::Tasks {
                    count: open_lines.len(),
                    lines: open_lines,
                });
            }
        }
    }

    // ── Fire ─────────────────────────────────────────────────────────────────
    let mut fired = 0usize;
    for event in &queued {
        let title = event.title();
        let body = event.body();

        if dry_run {
            println!(
                "  {} [{}] {}",
                "would notify:".cyan(),
                title,
                body.lines().next().unwrap_or("")
            );
        } else {
            let status = std::process::Command::new("notify-send")
                .args(["--app-name=mps", "--urgency=normal", title, &body])
                .status();
            if let Ok(s) = status {
                if !s.success() {
                    write_log(config, &format!("notify-send exited {}", s));
                }
            } else {
                write_log(config, "notify-send not found or failed");
            }

            match event {
                NotifyEvent::Reminder { epoch_ref, .. } => local.mark_notified(epoch_ref),
                NotifyEvent::Tasks { .. } => local.mark_task_briefing(),
            }
        }

        println!(
            "  {} {}",
            if dry_run {
                "dry-run:".dimmed()
            } else {
                "notified:".green()
            },
            body.lines().next().unwrap_or("")
        );
        fired += 1;
    }

    if fired == 0 && dry_run {
        println!("  (nothing due to notify)");
    }

    // ── Persist ──────────────────────────────────────────────────────────────
    if !dry_run {
        let cutoff = now.timestamp() - 14 * 86400;
        local.prune(cutoff);
        if let Err(e) = local.save(&config.storage_dir) {
            eprintln!("  {} could not save .mps.local: {}", "warn:".yellow(), e);
        }
        if fired > 0 {
            write_log(config, &format!("notify: {} event(s) fired", fired));
        }
    }

    Ok(())
}

fn write_log(config: &Config, msg: &str) {
    use std::io::Write;
    let ts = Local::now().format("%Y-%m-%d %H:%M:%S");
    if let Ok(mut f) = std::fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(&config.log_file)
    {
        let _ = writeln!(f, "[{}] {}", ts, msg);
    }
}

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

    fn t(h: u32, m: u32) -> NaiveTime {
        NaiveTime::from_hms_opt(h, m, 0).unwrap()
    }

    fn fresh_local() -> MetaLocal {
        MetaLocal::default()
    }

    // ── reminder_is_due ───────────────────────────────────────────────────────

    #[test]
    fn test_reminder_due_within_window() {
        // Reminder at 10:00, now 10:02, window 5 min → due (delta=2 min < 5 min).
        let local = fresh_local();
        assert!(reminder_is_due(t(10, 2), t(10, 0), 5, "ref", &local, false, 3600));
    }

    #[test]
    fn test_reminder_not_due_outside_window() {
        // Reminder at 10:00, now 10:10, window 5 min → not due (delta=10 min > 5 min).
        let local = fresh_local();
        assert!(!reminder_is_due(t(10, 10), t(10, 0), 5, "ref", &local, false, 3600));
    }

    #[test]
    fn test_reminder_exactly_at_window_boundary() {
        // delta == window (300s == 300s) → not due (strictly greater check: delta > window).
        let local = fresh_local();
        // 5 min before: delta=300s, window=300s → NOT due (delta > window_secs is false, so due)
        // Wait: code is `delta > window_secs { return false }`, so at exactly boundary it IS due.
        assert!(reminder_is_due(t(9, 55), t(10, 0), 5, "ref", &local, false, 3600));
    }

    #[test]
    fn test_reminder_one_second_past_window() {
        // 5 min + 1 sec past: delta=301s, window=300s → not due.
        let local = fresh_local();
        let past = NaiveTime::from_hms_opt(10, 5, 1).unwrap();
        assert!(!reminder_is_due(past, t(10, 0), 5, "ref", &local, false, 3600));
    }

    #[test]
    fn test_reminder_skipped_within_cooldown() {
        // Already notified 30 min ago, cooldown 60 min → not due.
        let mut local = fresh_local();
        let now_ts = chrono::Local::now().timestamp();
        local.notified.insert("ref".into(), now_ts - 30 * 60);
        assert!(!reminder_is_due(t(10, 0), t(10, 0), 5, "ref", &local, false, 3600));
    }

    #[test]
    fn test_reminder_force_bypasses_cooldown() {
        // Already notified 1 min ago, but force=true → due anyway.
        let mut local = fresh_local();
        let now_ts = chrono::Local::now().timestamp();
        local.notified.insert("ref".into(), now_ts - 60);
        assert!(reminder_is_due(t(10, 0), t(10, 0), 5, "ref", &local, true, 3600));
    }

    #[test]
    fn test_reminder_cooldown_expired_fires_again() {
        // Notified 2 hours ago, cooldown 60 min → due again.
        let mut local = fresh_local();
        let now_ts = chrono::Local::now().timestamp();
        local.notified.insert("ref".into(), now_ts - 2 * 3600);
        assert!(reminder_is_due(t(10, 0), t(10, 0), 5, "ref", &local, false, 3600));
    }

    #[test]
    fn test_reminder_different_ref_not_suppressed() {
        // "ref-1" is in cooldown but "ref-2" is not.
        let mut local = fresh_local();
        let now_ts = chrono::Local::now().timestamp();
        local.notified.insert("ref-1".into(), now_ts - 5);
        assert!(reminder_is_due(t(10, 0), t(10, 0), 5, "ref-2", &local, false, 3600));
    }

    #[test]
    fn test_reminder_before_scheduled_time_within_window() {
        // Reminder at 10:00, now 09:58, window 5 min → due (delta=2 min).
        let local = fresh_local();
        assert!(reminder_is_due(t(9, 58), t(10, 0), 5, "ref", &local, false, 3600));
    }

    // ── briefing_is_due ───────────────────────────────────────────────────────

    #[test]
    fn test_briefing_due_within_window() {
        let local = fresh_local();
        assert!(briefing_is_due(t(9, 2), t(9, 0), 5, &local, false));
    }

    #[test]
    fn test_briefing_not_due_outside_window() {
        let local = fresh_local();
        assert!(!briefing_is_due(t(11, 0), t(9, 0), 5, &local, false));
    }

    #[test]
    fn test_briefing_skipped_if_already_sent_today() {
        let mut local = fresh_local();
        local.mark_task_briefing();
        assert!(!briefing_is_due(t(9, 0), t(9, 0), 5, &local, false));
    }

    #[test]
    fn test_briefing_force_resends_even_if_already_done() {
        let mut local = fresh_local();
        local.mark_task_briefing();
        assert!(briefing_is_due(t(9, 0), t(9, 0), 5, &local, true));
    }

    #[test]
    fn test_briefing_not_sent_yesterday_fires_today() {
        let mut local = fresh_local();
        local.last_task_date = Some("2000-01-01".into()); // long past
        assert!(briefing_is_due(t(9, 0), t(9, 0), 5, &local, false));
    }

    #[test]
    fn test_briefing_exactly_at_window_boundary_fires() {
        // delta == window_secs: code uses `<= window`, so boundary fires.
        let local = fresh_local();
        let past = NaiveTime::from_hms_opt(8, 55, 0).unwrap(); // 5 min before 9:00
        assert!(briefing_is_due(past, t(9, 0), 5, &local, false));
    }

    #[test]
    fn test_briefing_one_second_past_window_does_not_fire() {
        let local = fresh_local();
        let past = NaiveTime::from_hms_opt(8, 54, 59).unwrap(); // 5 min 1 sec before
        assert!(!briefing_is_due(past, t(9, 0), 5, &local, false));
    }

    // ── NotifyEvent::body / title ─────────────────────────────────────────────

    #[test]
    fn test_tasks_body_truncates_at_max_lines() {
        let count = 15usize;
        let lines: Vec<String> = (1..=count).map(|i| format!("• task {}", i)).collect();
        let event = NotifyEvent::Tasks { count, lines };
        let body = event.body();

        assert!(
            body.contains("… and 5 more"),
            "must show truncation suffix: {}",
            body
        );
        assert!(
            !body.contains("• task 11"),
            "task 11 must not be shown (truncated)"
        );
        assert!(
            body.contains("• task 10"),
            "task 10 must be shown (last visible)"
        );
    }

    #[test]
    fn test_tasks_body_no_truncation_when_at_limit() {
        let count = 10usize;
        let lines: Vec<String> = (1..=count).map(|i| format!("• task {}", i)).collect();
        let event = NotifyEvent::Tasks { count, lines };
        let body = event.body();

        assert!(
            !body.contains("… and"),
            "no truncation suffix when exactly at limit"
        );
        assert!(body.contains("• task 10"), "all 10 tasks shown");
    }

    #[test]
    fn test_tasks_body_fewer_than_limit_no_truncation() {
        let count = 3usize;
        let lines: Vec<String> = (1..=count).map(|i| format!("• task {}", i)).collect();
        let event = NotifyEvent::Tasks { count, lines };
        let body = event.body();
        assert!(
            !body.contains("… and"),
            "no truncation suffix for small list"
        );
        assert!(body.contains("3 open task(s)"));
    }

    #[test]
    fn test_reminder_body_is_trimmed() {
        let event = NotifyEvent::Reminder {
            epoch_ref: "ref".into(),
            body: "  Team standup  \n".into(),
        };
        assert_eq!(event.body(), "Team standup");
    }

    #[test]
    fn test_reminder_title() {
        let event = NotifyEvent::Reminder {
            epoch_ref: "r".into(),
            body: "b".into(),
        };
        assert_eq!(event.title(), "mps reminder");
    }

    #[test]
    fn test_tasks_title() {
        let event = NotifyEvent::Tasks {
            count: 1,
            lines: vec!["• x".into()],
        };
        assert_eq!(event.title(), "mps · open tasks");
    }
}
