// SPDX-License-Identifier: GPL-3.0-or-later
/*
File: cfait/src/model/adapter.rs

This file implements ICS <-> Task mapping for Cfait.
It primarily contains:
 - to_ics: convert Task -> VTODO ICS
 - from_ics: parse VTODO ICS -> Task
 - to_event_ics: produce companion VEVENT ICS for calendar integration
*/

use crate::model::item::{Alarm, AlarmTrigger, DateType, RawProperty, Task, TaskStatus};
use chrono::{Datelike, NaiveDate, NaiveDateTime, TimeZone, Utc};
use icalendar::{Calendar, CalendarComponent, Component, Event, Todo, TodoStatus};
use uuid::Uuid;

/// List of property keys we explicitly handle when mapping to/from ICS.
const HANDLED_KEYS: &[&str] = &[
    "UID",
    "SUMMARY",
    "DESCRIPTION",
    "STATUS",
    "PRIORITY",
    "DUE",
    "DTSTART",
    "RRULE",
    "DURATION",
    "X-ESTIMATED-DURATION",
    "PERCENT-COMPLETE",
    "CATEGORIES",
    "RELATED-TO",
    "SEQUENCE",
    "PRODID",
    "VERSION",
    "CALSCALE",
    "RECURRENCE-ID",
    "LOCATION",
    "URL",
    "GEO",
    "X-CFAIT-CREATE-EVENT",
    "X-TIME-SPENT",
    "X-LAST-START",
    "X-CFAIT-SESSION",
    "EXDATE",
    "X-CFAIT-ESTIMATED-DURATION-MAX",
    "X-CFAIT-FUZZY-START",
    "X-CFAIT-FUZZY-DUE",
    "X-CFAIT-COLLAPSED",
    "X-CFAIT-PINNED",
];

pub struct IcsAdapter;

impl IcsAdapter {
    fn fix_rrule_until(rrule: &str, is_time: bool) -> String {
        let parts: Vec<&str> = rrule.split(';').collect();
        let mut new_parts = Vec::new();
        for part in parts {
            if let Some(until_val) = part.strip_prefix("UNTIL=") {
                if is_time {
                    if until_val.len() == 8 && !until_val.contains('T') {
                        new_parts.push(format!("UNTIL={}T235959Z", until_val));
                    } else if until_val.contains('T') && !until_val.ends_with('Z') {
                        new_parts.push(format!("UNTIL={}Z", until_val));
                    } else {
                        new_parts.push(part.to_string());
                    }
                } else {
                    if let Some(t_idx) = until_val.find('T') {
                        new_parts.push(format!("UNTIL={}", &until_val[..t_idx]));
                    } else {
                        new_parts.push(part.to_string());
                    }
                }
            } else {
                new_parts.push(part.to_string());
            }
        }
        new_parts.join(";")
    }

    pub fn to_ics(task: &Task) -> String {
        let mut todo = Todo::new();
        todo.add_property("UID", &task.uid);
        todo.summary(&task.summary);
        if !task.description.is_empty() {
            todo.description(&task.description);
        }
        todo.timestamp(Utc::now());
        todo.add_property("SEQUENCE", task.sequence.to_string());

        if let Some(loc) = &task.location {
            todo.add_property("LOCATION", loc);
        }
        if let Some(u) = &task.url {
            todo.add_property("URL", u);
        }
        if let Some(g) = &task.geo {
            let geo_val: String = g.replace(',', ";");
            todo.add_property("GEO", &geo_val);
        }

        match task.status {
            TaskStatus::NeedsAction => todo.status(TodoStatus::NeedsAction),
            TaskStatus::InProcess => todo.status(TodoStatus::InProcess),
            TaskStatus::Completed => todo.status(TodoStatus::Completed),
            TaskStatus::Cancelled => todo.status(TodoStatus::Cancelled),
        };
        if let Some(pc) = task.percent_complete {
            todo.percent_complete(pc);
        }

        // --- STRICT RFC 5545 COMPLIANCE FOR DATES ---
        let mut emit_dtstart = task.dtstart.clone();
        let mut emit_due = task.due.clone();

        if let (Some(start), Some(due)) = (&emit_dtstart, &emit_due) {
            let start_is_time = matches!(start, DateType::Specific(_));
            let due_is_time = matches!(due, DateType::Specific(_));

            if start_is_time != due_is_time {
                let midnight = chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap();
                if !start_is_time {
                    emit_dtstart =
                        Some(DateType::Specific(start.to_utc_with_default_time(midnight)));
                }
                if !due_is_time {
                    emit_due = Some(DateType::Specific(due.to_utc_with_default_time(midnight)));
                }
            }
        }

        let dtstart_is_time = emit_dtstart
            .as_ref()
            .map(|d| matches!(d, DateType::Specific(_)))
            .or_else(|| {
                emit_due
                    .as_ref()
                    .map(|d| matches!(d, DateType::Specific(_)))
            })
            .unwrap_or(false);

        let default_utc_time = if let Some(DateType::Specific(dt)) = &emit_dtstart {
            dt.time()
        } else if let Some(DateType::Specific(dt)) = &emit_due {
            dt.time()
        } else {
            chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap()
        };

        if let Some(dt) = &emit_dtstart {
            match dt {
                DateType::AllDay(d) => {
                    let mut p = icalendar::Property::new("DTSTART", d.format("%Y%m%d").to_string());
                    p.add_parameter("VALUE", "DATE");
                    todo.append_property(p);
                }
                DateType::Specific(t) => {
                    todo.add_property("DTSTART", t.format("%Y%m%dT%H%M%SZ").to_string());
                }
                DateType::Month(y, m) => {
                    let d = NaiveDate::from_ymd_opt(*y, *m, 1).unwrap();
                    let mut p = icalendar::Property::new("DTSTART", d.format("%Y%m%d").to_string());
                    p.add_parameter("VALUE", "DATE");
                    todo.append_property(p);
                    todo.add_property("X-CFAIT-FUZZY-START", format!("{:04}-{:02}", y, m));
                }
                DateType::Year(y) => {
                    let d = NaiveDate::from_ymd_opt(*y, 1, 1).unwrap();
                    let mut p = icalendar::Property::new("DTSTART", d.format("%Y%m%d").to_string());
                    p.add_parameter("VALUE", "DATE");
                    todo.append_property(p);
                    todo.add_property("X-CFAIT-FUZZY-START", format!("{:04}", y));
                }
            }
        }

        if let Some(dt) = &emit_due {
            match dt {
                DateType::AllDay(d) => {
                    let mut p = icalendar::Property::new("DUE", d.format("%Y%m%d").to_string());
                    p.add_parameter("VALUE", "DATE");
                    todo.append_property(p);
                }
                DateType::Specific(t) => {
                    todo.add_property("DUE", t.format("%Y%m%dT%H%M%SZ").to_string());
                }
                DateType::Month(y, m) => {
                    let next_m = if *m == 12 { 1 } else { *m + 1 };
                    let next_y = if *m == 12 { *y + 1 } else { *y };
                    let d = NaiveDate::from_ymd_opt(next_y, next_m, 1)
                        .unwrap()
                        .pred_opt()
                        .unwrap();
                    let mut p = icalendar::Property::new("DUE", d.format("%Y%m%d").to_string());
                    p.add_parameter("VALUE", "DATE");
                    todo.append_property(p);
                    todo.add_property("X-CFAIT-FUZZY-DUE", format!("{:04}-{:02}", y, m));
                }
                DateType::Year(y) => {
                    let d = NaiveDate::from_ymd_opt(*y, 12, 31).unwrap();
                    let mut p = icalendar::Property::new("DUE", d.format("%Y%m%d").to_string());
                    p.add_parameter("VALUE", "DATE");
                    todo.append_property(p);
                    todo.add_property("X-CFAIT-FUZZY-DUE", format!("{:04}", y));
                }
            }
        }

        if let Some(mins) = task.estimated_duration {
            let is_all_day_start = emit_dtstart
                .as_ref()
                .map(|dt| matches!(dt, DateType::AllDay(_)))
                .unwrap_or(false);

            if emit_due.is_some() || is_all_day_start {
                todo.add_property("X-ESTIMATED-DURATION", format!("PT{}M", mins));
            } else {
                todo.add_property("DURATION", format!("PT{}M", mins));
            }

            if let Some(max) = task.estimated_duration_max {
                todo.add_property("X-CFAIT-ESTIMATED-DURATION-MAX", format!("PT{}M", max));
            }
        }

        if task.priority > 0 {
            let prio = if task.priority > 9 { 9 } else { task.priority };
            todo.priority(prio.into());
        }
        if let Some(rrule) = &task.rrule {
            let fixed_rrule = Self::fix_rrule_until(rrule, dtstart_is_time);
            todo.add_property("RRULE", &fixed_rrule);
        }

        for ex in &task.exdates {
            let is_time = matches!(ex, DateType::Specific(_));
            if dtstart_is_time && !is_time {
                let date = ex.to_date_naive();
                let dt = Utc.from_utc_datetime(&date.and_time(default_utc_time));
                todo.append_multi_property(icalendar::Property::new(
                    "EXDATE",
                    dt.format("%Y%m%dT%H%M%SZ").to_string(),
                ));
            } else if !dtstart_is_time && is_time {
                let date = ex.to_date_naive();
                let mut p = icalendar::Property::new("EXDATE", date.format("%Y%m%d").to_string());
                p.add_parameter("VALUE", "DATE");
                todo.append_multi_property(p);
            } else {
                match ex {
                    DateType::AllDay(d) => {
                        let mut p =
                            icalendar::Property::new("EXDATE", d.format("%Y%m%d").to_string());
                        p.add_parameter("VALUE", "DATE");
                        todo.append_multi_property(p);
                    }
                    DateType::Specific(dt) => {
                        todo.append_multi_property(icalendar::Property::new(
                            "EXDATE",
                            dt.format("%Y%m%dT%H%M%SZ").to_string(),
                        ));
                    }
                    DateType::Month(y, m) => {
                        let d = NaiveDate::from_ymd_opt(*y, *m, 1).unwrap();
                        let mut p =
                            icalendar::Property::new("EXDATE", d.format("%Y%m%d").to_string());
                        p.add_parameter("VALUE", "DATE");
                        todo.append_multi_property(p);
                    }
                    DateType::Year(y) => {
                        let d = NaiveDate::from_ymd_opt(*y, 1, 1).unwrap();
                        let mut p =
                            icalendar::Property::new("EXDATE", d.format("%Y%m%d").to_string());
                        p.add_parameter("VALUE", "DATE");
                        todo.append_multi_property(p);
                    }
                }
            }
        }

        if let Some(p_uid) = &task.parent_uid {
            let p_uid_str: String = p_uid.as_str().into();
            let prop = icalendar::Property::new("RELATED-TO", &p_uid_str);
            todo.append_multi_property(prop);
        }
        for dep_uid in &task.dependencies {
            let mut prop = icalendar::Property::new("RELATED-TO", dep_uid);
            prop.add_parameter("RELTYPE", "DEPENDS-ON");
            todo.append_multi_property(prop);
        }
        for related_uid in &task.related_to {
            let mut prop = icalendar::Property::new("RELATED-TO", related_uid);
            prop.add_parameter("RELTYPE", "SIBLING");
            todo.append_multi_property(prop);
        }

        if let Some(create_event) = task.create_event {
            todo.add_property(
                "X-CFAIT-CREATE-EVENT",
                if create_event { "TRUE" } else { "FALSE" },
            );
        }

        if task.collapsed {
            todo.add_property("X-CFAIT-COLLAPSED", "TRUE");
        }
        if task.pinned {
            todo.add_property("X-CFAIT-PINNED", "TRUE");
        }

        // Emit time-tracking properties (if present)
        if task.time_spent_seconds > 0 {
            todo.add_property("X-TIME-SPENT", task.time_spent_seconds.to_string());
        }
        if let Some(ts) = task.last_started_at {
            todo.add_property("X-LAST-START", ts.to_string());
        }

        for raw in &task.unmapped_properties {
            if raw.key == "DTSTAMP" {
                continue;
            }
            let mut prop = icalendar::Property::new(&raw.key, &raw.value);
            for (k, v) in &raw.params {
                prop.add_parameter(k, v);
            }
            todo.append_property(prop);
        }

        let mut calendar = Calendar::new();
        calendar.push(todo);
        let mut ics = calendar.to_string();

        // Safely serialize work sessions to the server
        if !task.sessions.is_empty() {
            let mut session_lines = String::new();
            for session in &task.sessions {
                session_lines.push_str(&format!(
                    "X-CFAIT-SESSION:{}/{}\r\n",
                    session.start, session.end
                ));
            }
            if let Some(idx) = ics.rfind("END:VTODO") {
                let (start, end) = ics.split_at(idx);
                let mut buffer = String::with_capacity(ics.len() + session_lines.len());
                buffer.push_str(start);
                buffer.push_str(&session_lines);
                buffer.push_str(end);
                ics = buffer;
            }
        }

        if !task.categories.is_empty() {
            let escaped_cats: Vec<String> = task
                .categories
                .iter()
                .map(|c: &String| {
                    c.replace('\\', "\\\\")
                        .replace(',', "\\,")
                        .replace('\n', "\\n")
                        .replace('\r', "")
                })
                .collect();
            let cat_line = format!("CATEGORIES:{}", escaped_cats.join(","));
            if let Some(idx) = ics.rfind("END:VTODO") {
                let (start, end) = ics.split_at(idx);
                let mut buffer = String::with_capacity(ics.len() + cat_line.len() + 2);
                buffer.push_str(start);
                buffer.push_str(&cat_line);
                buffer.push_str("\r\n");
                buffer.push_str(end);
                ics = buffer;
            }
        }

        if !task.alarms.is_empty()
            && let Some(idx) = ics.rfind("END:VTODO")
        {
            let (start, end) = ics.split_at(idx);
            let mut buffer = String::with_capacity(ics.len() + 1024);
            buffer.push_str(start);

            for alarm in &task.alarms {
                buffer.push_str("BEGIN:VALARM\r\n");

                let safe_uid = alarm.uid.replace(['\n', '\r'], "");
                buffer.push_str(&format!("UID:{}\r\n", safe_uid));

                let safe_action = alarm.action.replace(['\n', '\r'], "");
                buffer.push_str(&format!("ACTION:{}\r\n", safe_action));

                if let Some(desc) = &alarm.description {
                    let safe_desc = desc
                        .replace('\\', "\\\\")
                        .replace('\n', "\\n")
                        .replace('\r', "");
                    buffer.push_str(&format!("DESCRIPTION:{}\r\n", safe_desc));
                } else {
                    buffer.push_str("DESCRIPTION:Reminder\r\n");
                }

                match alarm.trigger {
                    AlarmTrigger::Relative(mins) => {
                        let sign = if mins < 0 { "-" } else { "" };
                        buffer.push_str(&format!("TRIGGER:{}PT{}M\r\n", sign, mins.abs()));
                    }
                    AlarmTrigger::Absolute(dt) => {
                        buffer.push_str(&format!(
                            "TRIGGER;VALUE=DATE-TIME:{}\r\n",
                            dt.format("%Y%m%dT%H%M%SZ")
                        ));
                    }
                }

                if let Some(ack) = alarm.acknowledged {
                    let ack_str: String = ack.format("%Y%m%dT%H%M%SZ").to_string();
                    buffer.push_str(&format!("ACKNOWLEDGED:{}\r\n", ack_str));
                }

                if let Some(rel) = &alarm.related_to_uid {
                    let safe_rel = rel.replace(['\n', '\r'], "");
                    if let Some(rtype) = &alarm.relation_type {
                        let safe_rtype = rtype.replace(['\n', '\r'], "");
                        buffer.push_str(&format!(
                            "RELATED-TO;RELTYPE={}:{}\r\n",
                            safe_rtype, safe_rel
                        ));
                    } else {
                        buffer.push_str(&format!("RELATED-TO:{}\r\n", safe_rel));
                    }
                }

                buffer.push_str("END:VALARM\r\n");
            }
            buffer.push_str(end);
            ics = buffer;
        }

        if !task.raw_components.is_empty() {
            let extra_len: usize = task
                .raw_components
                .iter()
                .map(|s: &String| s.len() + 2)
                .sum();
            if let Some(idx) = ics.rfind("END:VCALENDAR") {
                let (start, end) = ics.split_at(idx);
                let mut buffer = String::with_capacity(ics.len() + extra_len);
                buffer.push_str(start);
                for raw in &task.raw_components {
                    buffer.push_str(raw);
                    if !raw.ends_with('\n') {
                        buffer.push_str("\r\n");
                    }
                }
                buffer.push_str(end);
                ics = buffer;
            }
        }

        ics
    }

    pub fn from_ics(
        raw_ics: &str,
        etag: String,
        href: String,
        calendar_href: String,
    ) -> Result<Task, String> {
        let calendar: Calendar = raw_ics.parse().map_err(|e| format!("Parse: {}", e))?;

        let mut master_todo: Option<&Todo> = None;
        let mut raw_components: Vec<String> = Vec::new();

        for component in &calendar.components {
            match component {
                CalendarComponent::Todo(t) => {
                    if t.properties().contains_key("RECURRENCE-ID") {
                        raw_components.push(t.to_string());
                    } else if master_todo.is_none() {
                        master_todo = Some(t);
                    } else {
                        raw_components.push(t.to_string());
                    }
                }
                CalendarComponent::Event(e) => raw_components.push(e.to_string()),
                CalendarComponent::Venue(v) => raw_components.push(v.to_string()),
                CalendarComponent::Other(o) => raw_components.push(o.to_string()),
                _ => {}
            }
        }

        let todo = master_todo.ok_or("No Master VTODO found in ICS".to_string())?;

        let get_prop = |key: &str| -> Option<String> {
            todo.properties().get(key).map(|p| p.value().to_string())
        };

        let uid = get_prop("UID").unwrap_or_default();
        let summary = get_prop("SUMMARY").unwrap_or_default();
        let description = get_prop("DESCRIPTION").unwrap_or_default();

        let status = if let Some(val) = get_prop("STATUS") {
            match val.trim().to_uppercase().as_str() {
                "COMPLETED" => TaskStatus::Completed,
                "IN-PROCESS" => TaskStatus::InProcess,
                "CANCELLED" => TaskStatus::Cancelled,
                _ => TaskStatus::NeedsAction,
            }
        } else {
            TaskStatus::NeedsAction
        };

        let priority = get_prop("PRIORITY")
            .and_then(|v| v.parse::<u8>().ok())
            .map(|p| if p > 9 { 9 } else { p })
            .unwrap_or(0);
        let sequence = get_prop("SEQUENCE")
            .and_then(|v| v.parse::<u32>().ok())
            .unwrap_or(0);
        let percent_complete = get_prop("PERCENT-COMPLETE").and_then(|v| v.parse::<u8>().ok());

        let location = get_prop("LOCATION");
        let url = get_prop("URL");
        let geo = get_prop("GEO").map(|s| s.replace(';', ","));

        let create_event =
            get_prop("X-CFAIT-CREATE-EVENT").and_then(|v| match v.trim().to_uppercase().as_str() {
                "TRUE" | "1" | "YES" => Some(true),
                "FALSE" | "0" | "NO" => Some(false),
                _ => None,
            });

        let collapsed = get_prop("X-CFAIT-COLLAPSED")
            .map(|v| v.trim().to_uppercase() == "TRUE")
            .unwrap_or(false);

        let pinned = get_prop("X-CFAIT-PINNED")
            .map(|v| v.trim().to_uppercase() == "TRUE")
            .unwrap_or(false);

        let fuzzy_due = get_prop("X-CFAIT-FUZZY-DUE");
        let fuzzy_start = get_prop("X-CFAIT-FUZZY-START");

        let parse_date_type =
            |prop: &icalendar::Property, fuzzy: Option<String>| -> Option<DateType> {
                let val = prop.value();

                // Check if it's a standard date first
                let standard_date = if val.len() >= 8 {
                    NaiveDate::parse_from_str(&val[0..8], "%Y%m%d").ok()
                } else {
                    None
                };

                // Stale Detection: Only use fuzzy metadata if it matches the actual property value
                if let Some(f) = fuzzy
                    && let Some(sd) = standard_date
                {
                    if f.len() == 7 && f == sd.format("%Y-%m").to_string() {
                        return Some(DateType::Month(sd.year(), sd.month()));
                    } else if f.len() == 4 && f == sd.format("%Y").to_string() {
                        return Some(DateType::Year(sd.year()));
                    }
                }

                // Fallback to standard parsing
                let is_date_param = prop
                    .params()
                    .get("VALUE")
                    .map(|v| v.value() == "DATE")
                    .unwrap_or(false);
                if is_date_param || val.len() == 8 {
                    NaiveDate::parse_from_str(val, "%Y%m%d")
                        .ok()
                        .map(DateType::AllDay)
                } else if val.ends_with('Z') {
                    NaiveDateTime::parse_from_str(val, "%Y%m%dT%H%M%SZ")
                        .ok()
                        .map(|d| DateType::Specific(Utc.from_utc_datetime(&d)))
                } else {
                    NaiveDateTime::parse_from_str(val, "%Y%m%dT%H%M%S")
                        .ok()
                        .map(|d| {
                            DateType::Specific(crate::model::item::safe_local_to_utc(
                                d.date(),
                                d.time(),
                            ))
                        })
                }
            };

        let due = todo
            .properties()
            .get("DUE")
            .and_then(|p| parse_date_type(p, fuzzy_due));
        let dtstart = todo
            .properties()
            .get("DTSTART")
            .and_then(|p| parse_date_type(p, fuzzy_start));
        let rrule = get_prop("RRULE");

        let mut exdates = Vec::new();
        if let Some(multi_props) = todo.multi_properties().get("EXDATE") {
            for prop in multi_props {
                let is_date = prop
                    .params()
                    .get("VALUE")
                    .map(|v| v.value() == "DATE")
                    .unwrap_or(false);
                let val_str = prop.value();
                for part in val_str.split(',') {
                    let part = part.trim();
                    if part.is_empty() {
                        continue;
                    }

                    if is_date || part.len() == 8 {
                        if let Ok(d) = NaiveDate::parse_from_str(part, "%Y%m%d") {
                            exdates.push(DateType::AllDay(d));
                        }
                    } else if part.ends_with('Z') {
                        if let Ok(dt) = NaiveDateTime::parse_from_str(part, "%Y%m%dT%H%M%SZ") {
                            exdates.push(DateType::Specific(Utc.from_utc_datetime(&dt)));
                        }
                    } else if let Ok(dt) = NaiveDateTime::parse_from_str(part, "%Y%m%dT%H%M%S") {
                        exdates.push(DateType::Specific(crate::model::item::safe_local_to_utc(
                            dt.date(),
                            dt.time(),
                        )));
                    }
                }
            }
        }

        let parse_ics_duration = |val: &str| -> i32 {
            let mut minutes: i32 = 0;
            let mut num_buf = String::new();
            let mut in_time = false;
            for c in val.chars() {
                match c {
                    'P' | 'p' => {}
                    'T' | 't' => {
                        in_time = true;
                    }
                    d if d.is_ascii_digit() => {
                        num_buf.push(d);
                    }
                    unit if !num_buf.is_empty() => {
                        if let Ok(n) = num_buf.parse::<i32>() {
                            match unit {
                                'W' => minutes += n * 7 * 24 * 60,
                                'D' => minutes += n * 24 * 60,
                                'H' if in_time => minutes += n * 60,
                                'M' if in_time => minutes += n,
                                _ => {}
                            }
                        }
                        num_buf.clear();
                    }
                    _ => {}
                }
            }
            minutes
        };

        let parse_dur = |val: &str| -> Option<u32> {
            let mut minutes = 0u32;
            let mut num_buf = String::new();
            let mut in_time = false;
            for c in val.chars() {
                if c == 'T' {
                    in_time = true;
                } else if c.is_numeric() {
                    num_buf.push(c);
                } else if !num_buf.is_empty() {
                    let n = num_buf.parse::<u32>().unwrap_or(0);
                    match c {
                        'D' => minutes += n * 24 * 60,
                        'H' if in_time => minutes += n * 60,
                        'M' if in_time => minutes += n,
                        'W' => minutes += n * 7 * 24 * 60,
                        _ => {}
                    }
                    num_buf.clear();
                }
            }
            if minutes > 0 { Some(minutes) } else { None }
        };

        let mut estimated_duration = todo
            .properties()
            .get("X-ESTIMATED-DURATION")
            .and_then(|p: &icalendar::Property| parse_dur(p.value()));

        if estimated_duration.is_none() {
            estimated_duration = todo
                .properties()
                .get("DURATION")
                .and_then(|p: &icalendar::Property| parse_dur(p.value()));
        }

        let estimated_duration_max = todo
            .properties()
            .get("X-CFAIT-ESTIMATED-DURATION-MAX")
            .and_then(|p: &icalendar::Property| parse_dur(p.value()));

        let mut categories = Vec::new();
        if let Some(multi_props) = todo.multi_properties().get("CATEGORIES") {
            for prop in multi_props {
                let parts: Vec<String> = prop
                    .value()
                    .split(',')
                    .map(|s: &str| s.trim().to_string())
                    .filter(|s: &String| !s.is_empty())
                    .collect();
                categories.extend(parts);
            }
        }
        if let Some(prop) = todo.properties().get("CATEGORIES") {
            let parts: Vec<String> = prop
                .value()
                .split(',')
                .map(|s: &str| s.trim().to_string())
                .filter(|s: &String| !s.is_empty())
                .collect();
            categories.extend(parts);
        }
        categories.sort();
        categories.dedup();

        let mut parent_uid = None;
        let mut dependencies = Vec::new();
        let mut related_to = Vec::new();

        // Manually parse sessions from unfolded text to ensure we catch all of them
        // regardless of how icalendar crate groups X- properties.
        let mut manual_sessions = Vec::new();

        let unfolded = icalendar::parser::unfold(raw_ics);
        let mut in_vtodo = false;
        let mut in_valarm = false;

        for line in unfolded.lines() {
            let line = line.trim();
            let line_upper = line.to_uppercase(); // Use uppercase for safe checking

            if line_upper == "BEGIN:VTODO" {
                in_vtodo = true;
                continue;
            }
            if line_upper == "END:VTODO" {
                in_vtodo = false;
                continue;
            }
            if line_upper == "BEGIN:VALARM" {
                in_valarm = true;
                continue;
            }
            if line_upper == "END:VALARM" {
                in_valarm = false;
                continue;
            }

            if in_vtodo && !in_valarm {
                // Case-insensitive checks
                if line_upper.starts_with("RELATED-TO")
                    && let Some((raw_key, val)) = line.split_once(':')
                {
                    let parts: Vec<&str> = raw_key.split(';').collect();
                    let mut is_dep = false;
                    let mut is_sibling = false;

                    for param in parts.iter().skip(1) {
                        let param_u = param.to_uppercase();
                        if param_u.contains("RELTYPE") {
                            if param_u.contains("DEPENDS-ON") {
                                is_dep = true;
                            } else if param_u.contains("SIBLING") {
                                is_sibling = true;
                            }
                        }
                    }

                    let value = val.trim().to_string();
                    if is_dep {
                        if !dependencies.contains(&value) {
                            dependencies.push(value);
                        }
                    } else if is_sibling {
                        if !related_to.contains(&value) {
                            related_to.push(value);
                        }
                    } else {
                        // FIX: Only assign parent_uid to the first un-typed RELATED-TO.
                        // If the server stripped RELTYPE=SIBLING from subsequent relations,
                        // safely route them back to the siblings list.
                        if parent_uid.is_none() {
                            parent_uid = Some(value);
                        } else if !related_to.contains(&value) {
                            related_to.push(value);
                        }
                    }
                }

                // Manual session parsing
                if line_upper.starts_with("X-CFAIT-SESSION:")
                    && let Some((_, val)) = line.split_once(':')
                {
                    // Un-escape comma in case a previous version escaped it, and fallback to slash
                    let clean_val = val.replace("\\,", ",");
                    let parts = if clean_val.contains('/') {
                        clean_val.split_once('/')
                    } else {
                        clean_val.split_once(',')
                    };

                    if let Some((s_str, e_str)) = parts
                        && let (Ok(start), Ok(end)) =
                            (s_str.trim().parse::<i64>(), e_str.trim().parse::<i64>())
                    {
                        manual_sessions.push(crate::model::item::WorkSession { start, end });
                    }
                }
            }
        }

        let mut unmapped_properties = Vec::new();
        let to_raw = |prop: &icalendar::Property| -> RawProperty {
            let mut params = Vec::new();
            for (k, param) in prop.params().iter() {
                params.push((k.clone(), param.value().to_string()));
            }
            if !params.is_empty() {
                params.sort_unstable();
            }
            RawProperty {
                key: prop.key().to_string(),
                value: prop.value().to_string(),
                params,
            }
        };

        for (key, prop) in todo.properties() {
            if !HANDLED_KEYS.contains(&key.to_uppercase().as_str()) {
                unmapped_properties.push(to_raw(prop));
            }
        }
        for (key, props) in todo.multi_properties() {
            if !HANDLED_KEYS.contains(&key.to_uppercase().as_str()) {
                for prop in props {
                    unmapped_properties.push(to_raw(prop));
                }
            }
        }
        if !unmapped_properties.is_empty() {
            unmapped_properties
                .sort_unstable_by(|a, b| a.key.cmp(&b.key).then(a.value.cmp(&b.value)));
        }

        let mut alarms = Vec::new();
        let mut in_alarm = false;
        let mut current_alarm_lines: Vec<String> = Vec::new();

        for line in raw_ics.lines() {
            let trim = line.trim();
            if trim == "BEGIN:VALARM" {
                in_alarm = true;
                continue;
            }
            if trim == "END:VALARM" {
                in_alarm = false;
                let mut alarm = Alarm {
                    uid: Uuid::new_v4().to_string(),
                    action: "DISPLAY".to_string(),
                    trigger: AlarmTrigger::Relative(0),
                    description: None,
                    acknowledged: None,
                    related_to_uid: None,
                    relation_type: None,
                };

                for l in &current_alarm_lines {
                    if let Some((key, val)) = l.split_once(':') {
                        let k_upper = key.split(';').next().unwrap_or(key).to_uppercase();
                        match k_upper.as_str() {
                            "UID" => alarm.uid = val.trim().to_string(),
                            "ACTION" => alarm.action = val.trim().to_string(),
                            "DESCRIPTION" => alarm.description = Some(val.trim().to_string()),
                            "TRIGGER" => {
                                if val.contains('T') && !val.contains('P') {
                                    if let Ok(dt) =
                                        NaiveDateTime::parse_from_str(val.trim(), "%Y%m%dT%H%M%SZ")
                                    {
                                        alarm.trigger =
                                            AlarmTrigger::Absolute(Utc.from_utc_datetime(&dt));
                                    }
                                } else {
                                    let v_trim = val.trim();
                                    let is_neg = v_trim.starts_with('-');
                                    let abs_mins = parse_ics_duration(if is_neg {
                                        &v_trim[1..]
                                    } else {
                                        v_trim
                                    });
                                    alarm.trigger = AlarmTrigger::Relative(if is_neg {
                                        -abs_mins
                                    } else {
                                        abs_mins
                                    });
                                }
                            }
                            "ACKNOWLEDGED" => {
                                if let Ok(dt) =
                                    NaiveDateTime::parse_from_str(val.trim(), "%Y%m%dT%H%M%SZ")
                                {
                                    alarm.acknowledged = Some(Utc.from_utc_datetime(&dt));
                                }
                            }
                            "RELATED-TO" => {
                                alarm.related_to_uid = Some(val.trim().to_string());
                                if key.contains("RELTYPE=SNOOZE") {
                                    alarm.relation_type = Some("SNOOZE".to_string());
                                }
                            }
                            _ => {}
                        }
                    }
                }
                alarms.push(alarm);
                current_alarm_lines.clear();
                continue;
            }
            if in_alarm {
                current_alarm_lines.push(line.to_string());
            }
        }

        // Parse time-tracking fields from properties
        let time_spent_seconds = get_prop("X-TIME-SPENT")
            .and_then(|v| v.parse::<u64>().ok())
            .unwrap_or(0);

        let last_started_at = get_prop("X-LAST-START").and_then(|v| v.parse::<i64>().ok());

        // Sort & dedup manually parsed sessions
        manual_sessions.sort_by_key(|s| s.start);
        manual_sessions.dedup();

        Ok(Task {
            uid,
            summary,
            description,
            status,
            estimated_duration,
            estimated_duration_max,
            due,
            dtstart,
            alarms,
            exdates,
            priority,
            percent_complete,
            parent_uid,
            dependencies,
            related_to,
            etag,
            href,
            calendar_href,
            categories,
            depth: 0,
            rrule,
            location,
            url,
            geo,
            collapsed,
            pinned,
            time_spent_seconds,
            last_started_at,
            sessions: manual_sessions, // Use manual parsing result
            unmapped_properties,
            sequence,
            raw_alarms: Vec::new(),
            raw_components,
            create_event,
            is_blocked: false,
            is_implicitly_blocked: false,
            is_implicitly_future: false,
            has_subtasks: false,
            has_visible_subtasks: false,
            sort_rank: 0,
            effective_priority: 0,
            effective_due: None,
            effective_dtstart: None,
            visible_categories: Vec::new(),
            visible_location: None,
            has_blocking_tasks: false,
            has_related_tasks: false,
            is_future_start: false,
            is_overdue: false,
        })
    }

    pub fn to_event_ics(task: &Task) -> Vec<(String, String)> {
        let mut results = Vec::new();
        let base_uid = format!("evt-{}", task.uid);

        let has_sessions = !task.sessions.is_empty();

        // 1. GENERATE WORK SESSIONS (The "Past" blocks for time tracking)
        // These are always generated if present into separate ICS files.
        for (index, session) in task.sessions.iter().enumerate() {
            let start_utc =
                chrono::DateTime::from_timestamp(session.start, 0).unwrap_or_else(Utc::now);
            let end_utc = chrono::DateTime::from_timestamp(session.end, 0).unwrap_or_else(Utc::now);

            // Skip invalid sessions
            if end_utc <= start_utc {
                continue;
            }

            let suffix = format!("-session-{}", index);
            let uid = format!("{}{}", base_uid, suffix);

            let mut session_event = Event::new();
            session_event.add_property("UID", &uid);
            session_event.summary(&format!("⚙ {}", task.summary));
            session_event.timestamp(Utc::now());
            session_event.add_property("STATUS", "CONFIRMED");
            session_event.add_property("SEQUENCE", task.sequence.to_string());

            let duration_mins = (end_utc - start_utc).num_minutes();
            let desc = format!(
                "Work session recorded by Cfait.\nDuration: {}m",
                duration_mins
            );
            session_event.description(&desc);

            session_event.add_property("DTSTART", start_utc.format("%Y%m%dT%H%M%SZ").to_string());
            session_event.add_property("DTEND", end_utc.format("%Y%m%dT%H%M%SZ").to_string());

            let mut calendar = Calendar::new();
            calendar.push(session_event);

            results.push((suffix, calendar.to_string()));
        }

        // 2. GENERATE MAIN EVENT (Planning or Completed)
        if task.status == TaskStatus::Completed && !has_sessions {
            // Completed event: show when it was completed as a short event.
            let explicit_completion = task.completion_date().or_else(|| {
                task.unmapped_properties
                    .iter()
                    .find(|p| p.key == "LAST-MODIFIED")
                    .and_then(|p| {
                        NaiveDateTime::parse_from_str(&p.value, "%Y%m%dT%H%M%SZ")
                            .ok()
                            .map(|ndt| Utc.from_utc_datetime(&ndt))
                    })
            });

            if let Some(completed_at) = explicit_completion {
                let duration_mins = if task.time_spent_seconds > 0 && !has_sessions {
                    (task.time_spent_seconds / 60).max(1) as i64
                } else {
                    task.estimated_duration.unwrap_or(60) as i64
                };
                let start_at = completed_at - chrono::Duration::minutes(duration_mins);

                let ics = Self::make_single_event_ics(
                    task,
                    "",
                    DateType::Specific(start_at),
                    DateType::Specific(completed_at),
                );
                results.push(("".to_string(), ics));
            } else if let Some(due) = &task.due {
                let start_dt = match due {
                    DateType::AllDay(d) => DateType::AllDay(*d),
                    DateType::Specific(dt) => {
                        let duration_mins = if task.time_spent_seconds > 0 && !has_sessions {
                            (task.time_spent_seconds / 60).max(1) as i64
                        } else {
                            task.estimated_duration.unwrap_or(60) as i64
                        };
                        DateType::Specific(*dt - chrono::Duration::minutes(duration_mins))
                    }
                    DateType::Month(y, m) => DateType::Month(*y, *m),
                    DateType::Year(y) => DateType::Year(*y),
                };
                let ics = Self::make_single_event_ics(task, "", start_dt, due.clone());
                results.push(("".to_string(), ics));
            } else if let Some(start) = &task.dtstart {
                let end_dt = match start {
                    DateType::AllDay(d) => DateType::AllDay(*d),
                    DateType::Specific(dt) => {
                        let duration_mins = if task.time_spent_seconds > 0 && !has_sessions {
                            (task.time_spent_seconds / 60).max(1) as i64
                        } else {
                            task.estimated_duration.unwrap_or(60) as i64
                        };
                        DateType::Specific(*dt + chrono::Duration::minutes(duration_mins))
                    }
                    DateType::Month(y, m) => DateType::Month(*y, *m),
                    DateType::Year(y) => DateType::Year(*y),
                };
                let ics = Self::make_single_event_ics(task, "", start.clone(), end_dt);
                results.push(("".to_string(), ics));
            }
        } else if (task.due.is_some() || task.dtstart.is_some())
            && task.status != TaskStatus::Completed
        {
            // Calculate duration to use (Estimate or Spent or Default 1h)
            let has_explicit_duration =
                task.estimated_duration.is_some() || (task.time_spent_seconds > 0 && !has_sessions);

            let duration_mins = if task.time_spent_seconds > 0 && !has_sessions {
                (task.time_spent_seconds / 60).max(1) as i64
            } else {
                task.estimated_duration.unwrap_or(60) as i64
            };

            match (&task.dtstart, &task.due) {
                (Some(s), Some(d)) => {
                    if has_explicit_duration {
                        // CASE A: Start + Due + Estimate/Spent
                        // If duration is present, we honor it specifically.
                        // We anchor to the DUE date (work back from deadline), or START date?
                        // Standard practice for 'scheduled work' is Start -> Start+Dur.
                        // Standard practice for 'deadline' is Due.
                        // If both exist + duration, usually implies "Scheduled block".
                        // Let's anchor to START if available, otherwise DUE?
                        // The prompt implied current behavior anchors to DUE.
                        // Let's keep that for now unless specific requirement changed.
                        match d {
                            DateType::AllDay(date) => {
                                results.push((
                                    "".to_string(),
                                    Self::make_single_event_ics(
                                        task,
                                        "",
                                        DateType::AllDay(*date),
                                        DateType::AllDay(*date),
                                    ),
                                ));
                            }
                            DateType::Specific(dt) => {
                                let start_val = *dt - chrono::Duration::minutes(duration_mins);
                                results.push((
                                    "".to_string(),
                                    Self::make_single_event_ics(
                                        task,
                                        "",
                                        DateType::Specific(start_val),
                                        DateType::Specific(*dt),
                                    ),
                                ));
                            }
                            DateType::Month(y, m) => {
                                let _date = NaiveDate::from_ymd_opt(*y, *m, 1).unwrap();
                                results.push((
                                    "".to_string(),
                                    Self::make_single_event_ics(
                                        task,
                                        "",
                                        DateType::Month(*y, *m),
                                        DateType::Month(*y, *m),
                                    ),
                                ));
                            }
                            DateType::Year(y) => {
                                let _date = NaiveDate::from_ymd_opt(*y, 1, 1).unwrap();
                                results.push((
                                    "".to_string(),
                                    Self::make_single_event_ics(
                                        task,
                                        "",
                                        DateType::Year(*y),
                                        DateType::Year(*y),
                                    ),
                                ));
                            }
                        }
                    } else {
                        // CASE B: Start + Due, NO Estimate
                        let s_date = s.to_date_naive();
                        let d_date = d.to_date_naive();
                        let diff = d_date - s_date;

                        if diff.num_days() <= 1 {
                            // <= 1 day apart -> Span length
                            let (final_start, final_end) = match (&s, &d) {
                                (DateType::AllDay(_), DateType::Specific(dt)) => (
                                    DateType::Specific(s.to_start_comparison_time()),
                                    DateType::Specific(*dt),
                                ),
                                (DateType::Specific(dt), DateType::AllDay(_)) => (
                                    DateType::Specific(*dt),
                                    DateType::Specific(d.to_comparison_time()),
                                ),
                                _ => (s.clone(), d.clone()),
                            };
                            results.push((
                                "".to_string(),
                                Self::make_single_event_ics(task, "", final_start, final_end),
                            ));
                        } else {
                            // > 1 day apart -> Two separate 1-day/1-hour events
                            let start_event_end = match s {
                                DateType::AllDay(date) => DateType::AllDay(*date),
                                DateType::Specific(dt) => {
                                    DateType::Specific(*dt + chrono::Duration::hours(1))
                                }
                                DateType::Month(y, m) => DateType::Month(*y, *m),
                                DateType::Year(y) => DateType::Year(*y),
                            };
                            let due_event_start = match d {
                                DateType::AllDay(date) => DateType::AllDay(*date),
                                DateType::Specific(dt) => {
                                    DateType::Specific(*dt - chrono::Duration::hours(1))
                                }
                                DateType::Month(y, m) => DateType::Month(*y, *m),
                                DateType::Year(y) => DateType::Year(*y),
                            };

                            results.push((
                                "-start".to_string(),
                                Self::make_single_event_ics(
                                    task,
                                    "-start",
                                    s.clone(),
                                    start_event_end,
                                ),
                            ));
                            results.push((
                                "-due".to_string(),
                                Self::make_single_event_ics(
                                    task,
                                    "-due",
                                    due_event_start,
                                    d.clone(),
                                ),
                            ));
                        }
                    }
                }
                (Some(s), None) => {
                    // Start Only: Start -> Start + Duration
                    match s {
                        DateType::AllDay(d) => {
                            results.push((
                                "".to_string(),
                                Self::make_single_event_ics(
                                    task,
                                    "",
                                    DateType::AllDay(*d),
                                    DateType::AllDay(*d),
                                ),
                            ));
                        }
                        DateType::Specific(dt) => {
                            results.push((
                                "".to_string(),
                                Self::make_single_event_ics(
                                    task,
                                    "",
                                    DateType::Specific(*dt),
                                    DateType::Specific(
                                        *dt + chrono::Duration::minutes(duration_mins),
                                    ),
                                ),
                            ));
                        }
                        DateType::Month(y, m) => {
                            results.push((
                                "".to_string(),
                                Self::make_single_event_ics(
                                    task,
                                    "",
                                    DateType::Month(*y, *m),
                                    DateType::Month(*y, *m),
                                ),
                            ));
                        }
                        DateType::Year(y) => {
                            results.push((
                                "".to_string(),
                                Self::make_single_event_ics(
                                    task,
                                    "",
                                    DateType::Year(*y),
                                    DateType::Year(*y),
                                ),
                            ));
                        }
                    }
                }
                (None, Some(d)) => {
                    // Due Only: Due - Duration -> Due
                    match d {
                        DateType::AllDay(d) => {
                            results.push((
                                "".to_string(),
                                Self::make_single_event_ics(
                                    task,
                                    "",
                                    DateType::AllDay(*d),
                                    DateType::AllDay(*d),
                                ),
                            ));
                        }
                        DateType::Specific(dt) => {
                            results.push((
                                "".to_string(),
                                Self::make_single_event_ics(
                                    task,
                                    "",
                                    DateType::Specific(
                                        *dt - chrono::Duration::minutes(duration_mins),
                                    ),
                                    DateType::Specific(*dt),
                                ),
                            ));
                        }
                        DateType::Month(y, m) => {
                            results.push((
                                "".to_string(),
                                Self::make_single_event_ics(
                                    task,
                                    "",
                                    DateType::Month(*y, *m),
                                    DateType::Month(*y, *m),
                                ),
                            ));
                        }
                        DateType::Year(y) => {
                            results.push((
                                "".to_string(),
                                Self::make_single_event_ics(
                                    task,
                                    "",
                                    DateType::Year(*y),
                                    DateType::Year(*y),
                                ),
                            ));
                        }
                    }
                }
                (None, None) => {}
            }
        }

        results
    }

    fn make_single_event_ics(
        task: &Task,
        suffix: &str,
        start_dt: DateType,
        end_dt: DateType,
    ) -> String {
        let base_uid = format!("evt-{}", task.uid);
        let uid = format!("{}{}", base_uid, suffix);

        let mut event = Event::new();
        event.add_property("UID", &uid);
        event.add_property("X-CFAIT-TASK-UID", &task.uid);

        let mut summary = match suffix {
            "-start" => format!("{} (start)", task.summary),
            "-due" => format!("{} (due)", task.summary),
            _ => task.summary.clone(),
        };

        if task.status == crate::model::TaskStatus::Completed {
            summary = format!("🗹 {}", summary);
        }

        event.summary(&summary);

        event.timestamp(Utc::now());
        event.add_property("SEQUENCE", task.sequence.to_string());

        let mut event_desc = String::new();
        if !task.description.is_empty() {
            event_desc.push_str(&task.description);
            event_desc.push_str("\n\n");
        }
        // Keep symbols (emoji/check) in code, but localize the human-readable text.
        event_desc.push_str(&format!("⚠️ {}\n", rust_i18n::t!("ics_event_auto_created")));
        if task.status == TaskStatus::Completed {
            event_desc.push_str(&format!(
                "✓ {}\n",
                rust_i18n::t!("ics_event_task_completed")
            ));
        } else {
            event_desc.push_str(&format!("{}\n", rust_i18n::t!("ics_event_auto_updated")));
            event_desc.push_str(&format!("{}\n", rust_i18n::t!("ics_event_changes_lost")));
        }
        event.description(&event_desc);

        if let Some(loc) = &task.location {
            event.add_property("LOCATION", loc);
        }
        if let Some(url) = &task.url {
            event.add_property("URL", url);
        }
        let is_time = matches!(start_dt, DateType::Specific(_));

        let default_utc_time = match &start_dt {
            DateType::Specific(dt) => dt.time(),
            _ => chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
        };

        if let Some(rrule) = &task.rrule {
            // Strip RRULE from events that represent a specific completed instance
            // so they don't incorrectly repeat on the calendar view.
            if task.status != TaskStatus::Completed {
                let fixed_rrule = Self::fix_rrule_until(rrule, is_time);
                event.add_property("RRULE", &fixed_rrule);

                // --- FIX 6: Copy EXDATEs to the calendar event so skipped instances disappear ---
                for ex in &task.exdates {
                    let ex_is_time = matches!(ex, DateType::Specific(_));
                    if is_time && !ex_is_time {
                        let date = ex.to_date_naive();
                        let dt = Utc.from_utc_datetime(&date.and_time(default_utc_time));
                        event.append_multi_property(icalendar::Property::new(
                            "EXDATE",
                            dt.format("%Y%m%dT%H%M%SZ").to_string(),
                        ));
                    } else if !is_time && ex_is_time {
                        let mut p = icalendar::Property::new(
                            "EXDATE",
                            ex.to_date_naive().format("%Y%m%d").to_string(),
                        );
                        p.add_parameter("VALUE", "DATE");
                        event.append_multi_property(p);
                    } else {
                        match ex {
                            DateType::AllDay(d) => {
                                let mut p = icalendar::Property::new(
                                    "EXDATE",
                                    d.format("%Y%m%d").to_string(),
                                );
                                p.add_parameter("VALUE", "DATE");
                                event.append_multi_property(p);
                            }
                            DateType::Specific(dt) => {
                                event.append_multi_property(icalendar::Property::new(
                                    "EXDATE",
                                    dt.format("%Y%m%dT%H%M%SZ").to_string(),
                                ));
                            }
                            DateType::Month(y, m) => {
                                let d = NaiveDate::from_ymd_opt(*y, *m, 1).unwrap();
                                let mut p = icalendar::Property::new(
                                    "EXDATE",
                                    d.format("%Y%m%d").to_string(),
                                );
                                p.add_parameter("VALUE", "DATE");
                                event.append_multi_property(p);
                            }
                            DateType::Year(y) => {
                                let d = NaiveDate::from_ymd_opt(*y, 1, 1).unwrap();
                                let mut p = icalendar::Property::new(
                                    "EXDATE",
                                    d.format("%Y%m%d").to_string(),
                                );
                                p.add_parameter("VALUE", "DATE");
                                event.append_multi_property(p);
                            }
                        }
                    }
                }
                // --------------------------------------------------------------------------------
            }
        }

        match task.status {
            TaskStatus::Cancelled => {
                event.add_property("STATUS", "CANCELLED");
            }
            _ => {
                event.add_property("STATUS", "CONFIRMED");
            }
        }

        match start_dt {
            DateType::AllDay(d) => {
                let mut p = icalendar::Property::new("DTSTART", d.format("%Y%m%d").to_string());
                p.add_parameter("VALUE", "DATE");
                event.append_property(p);
            }
            DateType::Specific(t) => {
                event.add_property("DTSTART", t.format("%Y%m%dT%H%M%SZ").to_string());
            }
            DateType::Month(y, m) => {
                let d = NaiveDate::from_ymd_opt(y, m, 1).unwrap();
                let mut p = icalendar::Property::new("DTSTART", d.format("%Y%m%d").to_string());
                p.add_parameter("VALUE", "DATE");
                event.append_property(p);
            }
            DateType::Year(y) => {
                let d = NaiveDate::from_ymd_opt(y, 1, 1).unwrap();
                let mut p = icalendar::Property::new("DTSTART", d.format("%Y%m%d").to_string());
                p.add_parameter("VALUE", "DATE");
                event.append_property(p);
            }
        }

        match end_dt {
            DateType::AllDay(d) => {
                let next_day = d + chrono::Duration::days(1);
                let mut p =
                    icalendar::Property::new("DTEND", next_day.format("%Y%m%d").to_string());
                p.add_parameter("VALUE", "DATE");
                event.append_property(p);
            }
            DateType::Specific(t) => {
                event.add_property("DTEND", t.format("%Y%m%dT%H%M%SZ").to_string());
            }
            DateType::Month(y, m) => {
                let next_m = if m == 12 { 1 } else { m + 1 };
                let next_y = if m == 12 { y + 1 } else { y };
                let d = NaiveDate::from_ymd_opt(next_y, next_m, 1).unwrap();
                let mut p = icalendar::Property::new("DTEND", d.format("%Y%m%d").to_string());
                p.add_parameter("VALUE", "DATE");
                event.append_property(p);
            }
            DateType::Year(y) => {
                let d = NaiveDate::from_ymd_opt(y + 1, 1, 1).unwrap();
                let mut p = icalendar::Property::new("DTEND", d.format("%Y%m%d").to_string());
                p.add_parameter("VALUE", "DATE");
                event.append_property(p);
            }
        }

        let mut calendar = Calendar::new();
        calendar.push(event);
        calendar.to_string()
    }
}
