use anyhow::Result;
use chrono::{DateTime, NaiveDate, TimeZone};
use chrono_tz::Tz;

#[derive(Debug, Clone, PartialEq)]
pub enum EventTime {
    AllDay(NaiveDate),
    DateTime(DateTime<Tz>),
}

#[derive(Debug, Clone, PartialEq)]
pub struct Event {
    pub summary: String,
    pub start: EventTime,
    pub calendar_name: String,
    pub color: String,
    pub rrule: Option<String>,
    pub uid: Option<String>,
}

impl Event {
    pub fn start_date(&self) -> NaiveDate {
        match &self.start {
            EventTime::AllDay(d) => *d,
            EventTime::DateTime(dt) => dt.with_timezone(&chrono::Local).date_naive(),
        }
    }
}

pub fn parse_ics(content: &str, calendar_name: &str, color: &str) -> Result<Vec<Event>> {
    use icalendar::{Calendar, CalendarComponent, Component, DatePerhapsTime};
    let calendar: Calendar = content.parse().map_err(|e| anyhow::anyhow!("{}", e))?;

    let mut events = Vec::new();
    for component in calendar.components.iter() {
        if let CalendarComponent::Event(ev) = component {
            let summary = ev.get_summary().unwrap_or("(no title)").to_string();

            let start = match ev.get_start() {
                Some(DatePerhapsTime::Date(nd)) => EventTime::AllDay(nd),
                Some(DatePerhapsTime::DateTime(dt)) => {
                    use icalendar::CalendarDateTime;
                    match dt {
                        CalendarDateTime::Floating(naive) => {
                            let local_dt = chrono::Local
                                .from_local_datetime(&naive)
                                .single()
                                .ok_or_else(|| anyhow::anyhow!("ambiguous datetime in local timezone"))?;
                            let utc_tz: Tz = "UTC".parse().unwrap();
                            let aware = utc_tz.from_utc_datetime(&local_dt.naive_utc());
                            EventTime::DateTime(aware)
                        }
                        CalendarDateTime::Utc(utc) => {
                            let tz: Tz = "UTC".parse().unwrap();
                            let aware = tz.from_utc_datetime(&utc.naive_utc());
                            EventTime::DateTime(aware)
                        }
                        CalendarDateTime::WithTimezone { date_time, tzid } => {
                            let tz: Tz = tzid.parse().unwrap_or_else(|_| {
                                eprintln!("warning: unknown timezone '{}', falling back to UTC", tzid);
                                "UTC".parse().unwrap()
                            });
                            let aware = tz
                                .from_local_datetime(&date_time)
                                .single()
                                .ok_or_else(|| anyhow::anyhow!("ambiguous datetime"))?;
                            EventTime::DateTime(aware)
                        }
                    }
                }
                None => continue,
            };

            let rrule = ev.property_value("RRULE").map(str::to_string);
            let uid = ev.property_value("UID").map(str::to_string);

            events.push(Event {
                summary,
                start,
                calendar_name: calendar_name.to_string(),
                color: color.to_string(),
                rrule,
                uid,
            });
        }
    }
    Ok(events)
}

pub fn expand_recurring(events: Vec<Event>, from: NaiveDate, to: NaiveDate) -> Vec<Event> {
    use rrule::RRuleSet;

    let mut result = Vec::new();

    for event in events {
        let Some(ref rrule_str) = event.rrule else {
            result.push(event);
            continue;
        };

        let dtstart = match &event.start {
            EventTime::AllDay(d) => {
                format!("DTSTART;VALUE=DATE:{}\n", d.format("%Y%m%d"))
            }
            EventTime::DateTime(dt) => {
                format!("DTSTART:{}\n", dt.naive_utc().format("%Y%m%dT%H%M%SZ"))
            }
        };

        let rrule_input = format!("{}RRULE:{}", dtstart, rrule_str);

        let rruleset: RRuleSet = match rrule_input.parse() {
            Ok(r) => r,
            Err(_) => {
                result.push(event);
                continue;
            }
        };

        // Safety cap: ~10 years of daily events. Guards against infinite rules.
        for dt in (&rruleset).into_iter().take(3_650) {
            let occurrence_date = dt.date_naive();
            if occurrence_date > to {
                break;
            }
            if occurrence_date < from {
                continue;
            }
            let new_start = match &event.start {
                EventTime::AllDay(_) => EventTime::AllDay(occurrence_date),
                EventTime::DateTime(_) => {
                    let utc_tz: Tz = "UTC".parse().unwrap();
                    EventTime::DateTime(utc_tz.from_utc_datetime(&dt.naive_utc()))
                }
            };
            result.push(Event {
                summary: event.summary.clone(),
                start: new_start,
                calendar_name: event.calendar_name.clone(),
                color: event.color.clone(),
                rrule: None,
                uid: event.uid.clone(),
            });
        }
    }

    // Deduplicate by (calendar_name, summary, local_date, local_hour, local_minute).
    // This removes duplicates that arise when an RRULE master event and a
    // RECURRENCE-ID override instance both expand to the same occurrence.
    let mut seen = std::collections::HashSet::new();
    result.retain(|e| {
        use chrono::Timelike;
        let date = e.start_date();
        let time_key = match &e.start {
            EventTime::AllDay(_) => (-1i32, -1i32),
            EventTime::DateTime(dt) => {
                let local = dt.with_timezone(&chrono::Local);
                (local.hour() as i32, local.minute() as i32)
            }
        };
        let identity = e.uid.as_deref().unwrap_or(&e.summary);
        seen.insert((e.calendar_name.clone(), identity.to_string(), date, time_key))
    });

    result
}

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

    fn make_allday_ics(date: &str, summary: &str) -> String {
        format!(
            "BEGIN:VCALENDAR\r\n\
             VERSION:2.0\r\n\
             BEGIN:VEVENT\r\n\
             DTSTART;VALUE=DATE:{}\r\n\
             SUMMARY:{}\r\n\
             END:VEVENT\r\n\
             END:VCALENDAR\r\n",
            date, summary
        )
    }

    fn make_datetime_ics(dtstart: &str, summary: &str) -> String {
        format!(
            "BEGIN:VCALENDAR\r\n\
             VERSION:2.0\r\n\
             BEGIN:VEVENT\r\n\
             DTSTART:{}\r\n\
             SUMMARY:{}\r\n\
             END:VEVENT\r\n\
             END:VCALENDAR\r\n",
            dtstart, summary
        )
    }

    fn make_tzid_ics(dtstart: &str, tzid: &str, summary: &str) -> String {
        format!(
            "BEGIN:VCALENDAR\r\n\
             VERSION:2.0\r\n\
             BEGIN:VEVENT\r\n\
             DTSTART;TZID={}:{}\r\n\
             SUMMARY:{}\r\n\
             END:VEVENT\r\n\
             END:VCALENDAR\r\n",
            tzid, dtstart, summary
        )
    }

    fn make_rrule_ics(dtstart: &str, rrule: &str, summary: &str) -> String {
        format!(
            "BEGIN:VCALENDAR\r\n\
             VERSION:2.0\r\n\
             BEGIN:VEVENT\r\n\
             DTSTART:{}\r\n\
             RRULE:{}\r\n\
             SUMMARY:{}\r\n\
             END:VEVENT\r\n\
             END:VCALENDAR\r\n",
            dtstart, rrule, summary
        )
    }

    #[test]
    fn test_parse_allday_event() {
        let ics = make_allday_ics("20260223", "All-day event");
        let events = parse_ics(&ics, "Test cal", "blue").unwrap();
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].summary, "All-day event");
        assert_eq!(
            events[0].start,
            EventTime::AllDay(NaiveDate::from_ymd_opt(2026, 2, 23).unwrap())
        );
        assert_eq!(events[0].calendar_name, "Test cal");
        assert_eq!(events[0].color, "blue");
        assert_eq!(events[0].rrule, None);
    }

    #[test]
    fn test_parse_datetime_event_utc() {
        let ics = make_datetime_ics("20260223T090000Z", "UTC meeting");
        let events = parse_ics(&ics, "Work", "red").unwrap();
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].summary, "UTC meeting");
        assert_eq!(
            events[0].start_date(),
            NaiveDate::from_ymd_opt(2026, 2, 23).unwrap()
        );
        assert_eq!(events[0].color, "red");
        assert_eq!(events[0].rrule, None);
    }

    #[test]
    fn test_parse_datetime_event_with_tzid() {
        let ics = make_tzid_ics("20260223T090000", "Asia/Tokyo", "Weekly meeting");
        let events = parse_ics(&ics, "Work", "green").unwrap();
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].summary, "Weekly meeting");
        assert_eq!(
            events[0].start_date(),
            NaiveDate::from_ymd_opt(2026, 2, 23).unwrap()
        );
        if let EventTime::DateTime(dt) = &events[0].start {
            assert_eq!(dt.hour(), 9);
        } else {
            panic!("expected DateTime");
        }
        assert_eq!(events[0].rrule, None);
    }

    #[test]
    fn test_parse_multibyte_summary() {
        let ics = make_allday_ics("20260224", "Cafe: special chars & symbols");
        let events = parse_ics(&ics, "school", "yellow").unwrap();
        assert_eq!(events[0].summary, "Cafe: special chars & symbols");
    }

    #[test]
    fn test_parse_multiple_events() {
        let ics = "BEGIN:VCALENDAR\r\n\
                   VERSION:2.0\r\n\
                   BEGIN:VEVENT\r\n\
                   DTSTART;VALUE=DATE:20260223\r\n\
                   SUMMARY:Event 1\r\n\
                   END:VEVENT\r\n\
                   BEGIN:VEVENT\r\n\
                   DTSTART:20260224T100000Z\r\n\
                   SUMMARY:Event 2\r\n\
                   END:VEVENT\r\n\
                   END:VCALENDAR\r\n";
        let events = parse_ics(ics, "family", "cyan").unwrap();
        assert_eq!(events.len(), 2);
    }

    #[test]
    fn test_parse_empty_calendar() {
        let ics = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nEND:VCALENDAR\r\n";
        let events = parse_ics(ics, "empty", "").unwrap();
        assert!(events.is_empty());
    }

    #[test]
    fn test_parse_rrule_field() {
        let ics = make_rrule_ics("20260216T090000Z", "FREQ=WEEKLY;BYDAY=MO", "Weekly meeting");
        let events = parse_ics(&ics, "Work", "blue").unwrap();
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].rrule, Some("FREQ=WEEKLY;BYDAY=MO".to_string()));
    }

    #[test]
    fn test_expand_non_recurring_unchanged() {
        let event = Event {
            summary: "meeting".to_string(),
            start: EventTime::AllDay(NaiveDate::from_ymd_opt(2026, 2, 18).unwrap()),
            calendar_name: "Work".to_string(),
            color: "blue".to_string(),
            rrule: None,
            uid: None,
        };
        let from = NaiveDate::from_ymd_opt(2026, 2, 16).unwrap();
        let to = NaiveDate::from_ymd_opt(2026, 2, 22).unwrap();
        let result = expand_recurring(vec![event.clone()], from, to);
        assert_eq!(result.len(), 1);
        assert_eq!(result[0], event);
    }

    #[test]
    fn test_expand_recurring_weekly() {
        // Weekly Monday event starting 2026-02-09; range [2026-02-16, 2026-02-22] → 1 hit
        let event = Event {
            summary: "Weekly meeting".to_string(),
            start: EventTime::DateTime(
                chrono_tz::UTC
                    .from_utc_datetime(
                        &NaiveDate::from_ymd_opt(2026, 2, 9)
                            .unwrap()
                            .and_hms_opt(9, 0, 0)
                            .unwrap(),
                    ),
            ),
            calendar_name: "Work".to_string(),
            color: "blue".to_string(),
            rrule: Some("FREQ=WEEKLY;BYDAY=MO".to_string()),
            uid: None,
        };
        let from = NaiveDate::from_ymd_opt(2026, 2, 16).unwrap();
        let to = NaiveDate::from_ymd_opt(2026, 2, 22).unwrap();
        let result = expand_recurring(vec![event], from, to);
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].summary, "Weekly meeting");
        assert_eq!(result[0].color, "blue");
        assert_eq!(result[0].start_date(), NaiveDate::from_ymd_opt(2026, 2, 16).unwrap());
        assert_eq!(result[0].rrule, None);
    }

    #[test]
    fn test_expand_recurring_deduplicates() {
        // Master (RRULE) + RECURRENCE-ID override for the same occurrence → 1 result
        let master = Event {
            summary: "Weekly meeting".to_string(),
            start: EventTime::DateTime(
                chrono_tz::UTC.from_utc_datetime(
                    &NaiveDate::from_ymd_opt(2026, 2, 9).unwrap().and_hms_opt(9, 0, 0).unwrap(),
                ),
            ),
            calendar_name: "Work".to_string(),
            color: "blue".to_string(),
            rrule: Some("FREQ=WEEKLY;BYDAY=MO".to_string()),
            uid: None,
        };
        let override_event = Event {
            summary: "Weekly meeting".to_string(),
            start: EventTime::DateTime(
                chrono_tz::UTC.from_utc_datetime(
                    &NaiveDate::from_ymd_opt(2026, 2, 16).unwrap().and_hms_opt(9, 0, 0).unwrap(),
                ),
            ),
            calendar_name: "Work".to_string(),
            color: "blue".to_string(),
            rrule: None,
            uid: None,
        };
        let from = NaiveDate::from_ymd_opt(2026, 2, 16).unwrap();
        let to = NaiveDate::from_ymd_opt(2026, 2, 16).unwrap();
        let result = expand_recurring(vec![master, override_event], from, to);
        assert_eq!(result.len(), 1);
    }

    #[test]
    fn test_expand_recurring_daily() {
        // Daily event starting 2026-02-20; range [2026-02-20, 2026-02-22] → 3 hits
        let event = Event {
            summary: "Daily standup".to_string(),
            start: EventTime::DateTime(
                chrono_tz::UTC
                    .from_utc_datetime(
                        &NaiveDate::from_ymd_opt(2026, 2, 20)
                            .unwrap()
                            .and_hms_opt(10, 0, 0)
                            .unwrap(),
                    ),
            ),
            calendar_name: "habits".to_string(),
            color: "green".to_string(),
            rrule: Some("FREQ=DAILY".to_string()),
            uid: None,
        };
        let from = NaiveDate::from_ymd_opt(2026, 2, 20).unwrap();
        let to = NaiveDate::from_ymd_opt(2026, 2, 22).unwrap();
        let result = expand_recurring(vec![event], from, to);
        assert_eq!(result.len(), 3);
        assert_eq!(result[0].start_date(), NaiveDate::from_ymd_opt(2026, 2, 20).unwrap());
        assert_eq!(result[1].start_date(), NaiveDate::from_ymd_opt(2026, 2, 21).unwrap());
        assert_eq!(result[2].start_date(), NaiveDate::from_ymd_opt(2026, 2, 22).unwrap());
    }
}
