use std::{
    collections::BTreeSet,
    process::{Command, Stdio},
    thread::sleep,
    time::{Duration, Instant},
};

use chrono::{Datelike, Local, NaiveDate, Timelike};
use serde_json::json;

const COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
const MONTH_NAMES: [&str; 13] = [
    "",
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December",
];

fn run_khal(args: &[&str]) -> Option<String> {
    let mut child = Command::new("khal")
        .args(args)
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .spawn()
        .ok()?;

    let deadline = Instant::now() + COMMAND_TIMEOUT;
    loop {
        match child.try_wait() {
            Ok(Some(_status)) => {
                let output = child.wait_with_output().ok()?;
                return Some(String::from_utf8_lossy(&output.stdout).trim().to_owned());
            }
            Ok(None) if Instant::now() >= deadline => {
                let _ = child.kill();
                let _ = child.wait();
                return None;
            }
            Ok(None) => sleep(Duration::from_millis(25)),
            Err(_) => return None,
        }
    }
}

fn days_in_month(year: i32, month: u32) -> u32 {
    let (next_year, next_month) = if month == 12 {
        (year + 1, 1)
    } else {
        (year, month + 1)
    };

    NaiveDate::from_ymd_opt(next_year, next_month, 1)
        .and_then(|date| date.pred_opt())
        .map_or(31, |date| date.day())
}

fn get_event_days(year: i32, month: u32) -> BTreeSet<u32> {
    let Some(first) = NaiveDate::from_ymd_opt(year, month, 1) else {
        return BTreeSet::new();
    };
    let Some(last) = NaiveDate::from_ymd_opt(year, month, days_in_month(year, month)) else {
        return BTreeSet::new();
    };

    let start = first.format("%d.%m.%Y").to_string();
    let end = last.format("%d.%m.%Y").to_string();
    let Some(stdout) = run_khal(&[
        "list",
        &start,
        &end,
        "--format",
        "{start-date}",
        "--day-format",
        "",
    ]) else {
        return BTreeSet::new();
    };

    stdout
        .lines()
        .filter_map(|line| NaiveDate::parse_from_str(line.trim(), "%d.%m.%Y").ok())
        .filter(|date| date.month() == month)
        .map(|date| date.day())
        .collect()
}

fn build_calendar(year: i32, month: u32, event_days: &BTreeSet<u32>, today: NaiveDate) -> String {
    let Some(first) = NaiveDate::from_ymd_opt(year, month, 1) else {
        return String::new();
    };

    let month_name = MONTH_NAMES
        .get(month as usize)
        .copied()
        .unwrap_or("Unknown");
    let mut lines = vec![
        format!(r##"<span color="#1e66f5"><b>{month_name} {year}</b></span>"##),
        r##"<span color="#8839ef"><b>Mo Tu We Th Fr Sa Su</b></span>"##.to_owned(),
    ];

    let mut week = vec!["  ".to_owned(); (first.weekday().number_from_monday() - 1) as usize];
    for day in 1..=days_in_month(year, month) {
        let day_string = if day == today.day() && month == today.month() && year == today.year() {
            format!(r##"<span color="#d20f39"><b><u>{day:2}</u></b></span>"##)
        } else if event_days.contains(&day) {
            format!(r##"<span color="#40a02b"><b>{day:2}</b></span>"##)
        } else {
            format!(r##"<span color="#4c4f69">{day:2}</span>"##)
        };
        week.push(day_string);

        if week.len() == 7 {
            lines.push(week.join(" "));
            week.clear();
        }
    }

    if !week.is_empty() {
        week.resize(7, "  ".to_owned());
        lines.push(week.join(" "));
    }

    lines.join("\n")
}

fn get_upcoming_events() -> String {
    run_khal(&["list", "today", "7d"]).unwrap_or_default()
}

fn main() {
    let now = Local::now();
    let today = now.date_naive();
    let event_days = get_event_days(today.year(), today.month());
    let calendar = build_calendar(today.year(), today.month(), &event_days, today);
    let events = get_upcoming_events();

    let tooltip = if events.is_empty() {
        calendar
    } else {
        format!(r##"{calendar}\n\n<span color="#1e66f5"><b>Upcoming</b></span>\n{events}"##)
    };

    println!(
        "{}",
        json!({
            "text": format!("{:02}:{:02}", now.hour(), now.minute()),
            "tooltip": tooltip,
            "alt": today.format("%Y-%m-%d").to_string(),
        })
    );
}
