// SPDX-License-Identifier: GPL-3.0-or-later
// File: ./src/model/parser.rs
/*
File: cfait/src/model/parser.rs
Logic for parsing smart input strings into task properties.
This file is recreated with updated handling for `done:` tokens to accept
either a full datetime in the token (`done:YYYY-MM-DD HH:MM`) or the older
date-only form with an optional separate time token following it.
*/

use crate::model::{Alarm, DateType, Task};
use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveTime, Utc};
use std::collections::{HashMap, HashSet};

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum SyntaxType {
    Text,
    Priority,
    DueDate,
    StartDate,
    Recurrence,
    Duration,
    Tag,
    Location,
    Url,
    Geo,
    Description,
    Reminder,
    Calendar, // +cal, -cal
    Pin,      // +pin, -pin
    Filter,   // is:done, < / > operators, duration filters, etc.
    Operator, // Boolean / operator tokens: |, -, (, ), AND/OR/NOT
}

#[derive(Debug)]
pub struct SyntaxToken {
    pub kind: SyntaxType,
    pub start: usize,
    pub end: usize,
}

pub fn parse_alias_values(input: &str) -> Vec<String> {
    let mut parts = Vec::new();
    let mut current = String::new();
    let mut in_quote = false;
    let mut escaped = false;
    let mut in_geo = false;

    for c in input.chars() {
        if escaped {
            current.push(c);
            escaped = false;
        } else if c == '\\' {
            escaped = true;
            current.push(c);
        } else if c == '"' {
            in_quote = !in_quote;
            current.push(c);
        } else if c == ',' && !in_quote {
            let trimmed = current.trim_start();
            if trimmed.starts_with("geo:") && !in_geo {
                in_geo = true;
                current.push(c);
            } else {
                parts.push(current.trim().to_string());
                current.clear();
                in_geo = false;
            }
        } else {
            current.push(c);
        }
    }
    if !current.trim().is_empty() {
        parts.push(current.trim().to_string());
    }
    parts.into_iter().filter(|s| !s.is_empty()).collect()
}

pub fn extract_inline_aliases(input: &str) -> (String, HashMap<String, Vec<String>>) {
    let parts = split_input_respecting_quotes(input);
    let mut cleaned_words = Vec::new();
    let mut new_aliases = HashMap::new();

    for (_, _, token) in parts {
        if token.contains(":=")
            && !token.starts_with('\\')
            && let Some((left, right)) = token.split_once(":=")
        {
            let mut key = String::new();
            let mut is_valid = false;

            // Case 1: Tag Alias (#tag:=...)
            if left.starts_with('#') {
                key = strip_quotes(left.trim_start_matches('#'));
                is_valid = !key.is_empty();
            }
            // Case 2: Location Alias (@@loc:=... or loc:loc:=...)
            else if left.starts_with("@@") || left.to_lowercase().starts_with("loc:") {
                let raw = if left.starts_with("@@") {
                    left.trim_start_matches('@')
                } else {
                    &left[4..]
                };
                let clean = strip_quotes(raw);
                if !clean.is_empty() {
                    // Store location aliases with prefix to distinguish from tags
                    key = format!("@@{}", clean);
                    is_valid = true;
                }
            }

            if is_valid {
                let tags = parse_alias_values(right);

                if !tags.is_empty() {
                    new_aliases.insert(key, tags);
                    // Keep the key part in the description as the 'primary' value
                    cleaned_words.push(left.to_string());
                    continue;
                }
            }
        }
        cleaned_words.push(token);
    }
    (cleaned_words.join(" "), new_aliases)
}

pub fn is_valid_geo(val: &str) -> bool {
    let stripped = strip_quotes(val);
    let s = stripped.trim();
    if s.eq_ignore_ascii_case("here") {
        return true;
    }

    let parts: Vec<&str> = s.split(',').collect();
    if parts.len() != 2 {
        return false;
    }

    let valid_part = |p: &str| -> bool {
        let p = p.trim();
        !p.is_empty()
            && p.chars()
                .all(|c| c.is_ascii_digit() || " .-°NSEWnsew".contains(c))
    };

    valid_part(parts[0]) && valid_part(parts[1])
}

pub fn validate_alias_integrity(
    new_key: &str,
    new_values: &[String],
    current_aliases: &HashMap<String, Vec<String>>,
) -> Result<(), String> {
    // 1. Normalize values into Keys (strip #, keep @@, check hierarchy)
    fn to_key(val: &str) -> Option<String> {
        if val.starts_with('#') {
            Some(strip_quotes(val.trim_start_matches('#')))
        } else if val.starts_with("@@") {
            Some(format!("@@{}", strip_quotes(val.trim_start_matches('@'))))
        } else if val.to_lowercase().starts_with("loc:") {
            Some(format!("@@{}", strip_quotes(&val[4..])))
        } else {
            None
        }
    }

    // 2. Check immediate self-reference
    if new_values
        .iter()
        .any(|v| to_key(v).as_deref() == Some(new_key))
    {
        return Err(format!("Alias '{}' cannot refer to itself.", new_key));
    }

    // 3. DFS Traversal to find cycles
    let mut stack: Vec<String> = new_values.iter().filter_map(|v| to_key(v)).collect();
    let mut visited_path = HashSet::new();

    while let Some(current_ref) = stack.pop() {
        if current_ref == new_key {
            return Err(format!(
                "Circular dependency: '{}' leads back to itself.",
                new_key
            ));
        }
        if visited_path.contains(&current_ref) {
            continue;
        }
        visited_path.insert(current_ref.clone());

        // --- HIERARCHY LOGIC START ---
        // We must mimic the runtime parser: if exact key missing, try parent key.
        let mut search = current_ref.as_str();
        loop {
            if let Some(children) = current_aliases.get(search) {
                // Found a definition! Add its children to stack.
                for child in children {
                    if let Some(k) = to_key(child) {
                        stack.push(k);
                    }
                }
                // In runtime, we stop after finding the first match in the hierarchy.
                // We must mirror that behavior here.
                break;
            }

            // Fallback: Try stripping the last segment (e.g., "A:B" -> "A")
            if let Some(idx) = search.rfind(':') {
                // Safety: Don't split inside the "@@" prefix if it's a location
                // e.g. "@@Home:Kitchen" -> "@@Home" (OK)
                // e.g. "@@:Kitchen" -> "@@" (Valid Key? Yes, theoretically)
                if search.starts_with("@@") && idx < 2 {
                    break;
                }
                search = &search[..idx];
            } else {
                break;
            }
        }
        // --- HIERARCHY LOGIC END ---
    }
    Ok(())
}

fn parse_time_string(s: &str) -> Option<NaiveTime> {
    let lower = s.to_lowercase();

    // Helper for 12h
    let parse_12h = |s: &str, is_pm: bool| -> Option<NaiveTime> {
        let (h, m) = if let Some((h_str, m_str)) = s.split_once(':') {
            (h_str.parse::<u32>().ok()?, m_str.parse::<u32>().ok()?)
        } else {
            (s.parse::<u32>().ok()?, 0)
        };
        if !(1..=12).contains(&h) || m > 59 {
            return None;
        }
        let h_24 = if h == 12 {
            if is_pm { 12 } else { 0 }
        } else if is_pm {
            h + 12
        } else {
            h
        };
        NaiveTime::from_hms_opt(h_24, m, 0)
    };

    if let Some(stripped) = lower.strip_suffix("am") {
        return parse_12h(stripped, false);
    }
    if let Some(stripped) = lower.strip_suffix("pm") {
        return parse_12h(stripped, true);
    }

    if let Some((h_str, m_str)) = lower.split_once(':') {
        let h = h_str.parse::<u32>().ok()?;
        let m = m_str.parse::<u32>().ok()?;
        return NaiveTime::from_hms_opt(h, m, 0);
    }

    None
}

pub fn parse_time_range(s: &str) -> Option<(NaiveTime, NaiveTime)> {
    let (start_str, end_str) = s.split_once('-')?;
    let start = parse_time_string(start_str)?;
    let end = parse_time_string(end_str)?;
    Some((start, end))
}

fn is_time_format(s: &str) -> bool {
    parse_time_string(s).is_some() || parse_time_range(s).is_some()
}

pub fn tokenize_smart_input(input: &str, is_search_query: bool) -> Vec<SyntaxToken> {
    let mut tokens = Vec::new();
    let words = split_input_respecting_quotes(input);

    let mut cursor = 0;
    let mut i = 0;
    let mut has_recurrence = false; // Track if we've seen a recurrence token

    while i < words.len() {
        let (start, end, word) = &words[i];
        if *start > cursor {
            tokens.push(SyntaxToken {
                kind: SyntaxType::Text,
                start: cursor,
                end: *start,
            });
        }

        let mut matched_kind = None;
        let mut words_consumed = 1;

        let word_lower = word.to_lowercase();

        // 0. Search-only filters (MOVED UP and EXPANDED)
        if is_search_query {
            // Check for Boolean Operators, parentheses, and textual boolean operators
            // Combine branches to avoid duplicate blocks (fixes clippy: if_same_then_else)
            if word == "|"
                || word == "("
                || word == ")"
                || word.eq_ignore_ascii_case("and")
                || word.eq_ignore_ascii_case("or")
                || word.eq_ignore_ascii_case("not")
            {
                matched_kind = Some(SyntaxType::Operator);
            } else {
                // Check for negation prefix "-"
                if word.starts_with('-') && word.len() > 1 {
                    // e.g. -#tag, -is:done
                    matched_kind = Some(SyntaxType::Filter);
                }
                // Existing Filter logic
                else if word_lower.starts_with("is:")
                    || ((word.starts_with('!')
                        || word.starts_with('~')
                        || word.starts_with('@')
                        || word.starts_with("due:")
                        || word.starts_with('^')
                        || word.starts_with("start:"))
                        && (word.contains('<') || word.contains('>')))
                {
                    matched_kind = Some(SyntaxType::Filter);
                }
            }
        }

        // 1. Recurrence
        if matched_kind.is_none()
            && (word == "@every" || word == "rec:every" || word == "@after")
            && i + 1 < words.len()
        {
            let next_token_str = words[i + 1].2.as_str();
            let next_next = if i + 2 < words.len() {
                Some(words[i + 2].2.as_str())
            } else {
                None
            };

            if let Some((_, _, consumed)) = parse_amount_and_unit(next_token_str, next_next, false)
            {
                matched_kind = Some(SyntaxType::Recurrence);
                words_consumed = 1 + 1 + consumed;
            } else {
                // Check for single weekday or comma-separated weekdays
                let parts: Vec<&str> = next_token_str.split(',').map(|s| s.trim()).collect();
                let all_weekdays = parts.iter().all(|part| parse_weekday_code(part).is_some());

                if all_weekdays && !parts.is_empty() {
                    matched_kind = Some(SyntaxType::Recurrence);
                    words_consumed = 2;
                }
            }
        } else if matched_kind.is_none()
            && (word == "@daily"
                || word == "@weekly"
                || word == "@monthly"
                || word == "@yearly"
                || word == "rec:daily"
                || word == "rec:weekly"
                || word == "rec:monthly"
                || word == "rec:yearly")
        {
            matched_kind = Some(SyntaxType::Recurrence);
            has_recurrence = true;
        }

        // Track if we just matched a recurrence token
        if matched_kind == Some(SyntaxType::Recurrence) {
            has_recurrence = true;
        }

        if matched_kind == Some(SyntaxType::Recurrence) {
            // Peek ahead: if the token immediately after the recurrence definition is a time,
            // consume it and include it in the recurrence highlighting.
            if i + words_consumed < words.len() {
                let potential_time = &words[i + words_consumed].2;
                if is_time_format(potential_time) {
                    words_consumed += 1;
                }
            }
        }

        // Check for until / except keywords (Recurrence extensions)
        // Only highlight if we've seen a recurrence token
        if matched_kind.is_none()
            && has_recurrence
            && (word_lower == "until" || word_lower == "except")
            && i + 1 < words.len()
        {
            let next_token_str = words[i + 1].2.as_str();

            // Check if it's a valid date-like token that can be followed by an optional time
            let is_list = if next_token_str.contains(',') {
                true // Comma-separated list is always a single token
            } else if parse_smart_date(next_token_str).is_some()
                || parse_weekday_date(next_token_str).is_some()
            {
                // This is a date. Check if the NEXT token is a time.
                if i + 2 < words.len() && is_time_format(&words[i + 2].2) {
                    words_consumed = 3; // Consume "except", "date", and "time"
                }
                true
            } else {
                parse_weekday_code(next_token_str).is_some()
                    || parse_month_code(next_token_str).is_some()
            };

            if is_list {
                matched_kind = Some(SyntaxType::Recurrence);
                // words_consumed already adjusted above for date+time case, otherwise default 2
                if words_consumed == 1 {
                    words_consumed = 2;
                }
            }
        }

        // 2. Dates
        if matched_kind.is_none() {
            // Check for ^@ prefix first
            let (is_start, clean_word) = if let Some(val) = word.strip_prefix("^@") {
                (true, val)
            } else if let Some(val) = word
                .strip_prefix("start:")
                .or_else(|| word.strip_prefix('^'))
            {
                (true, val)
            } else if let Some(val) = word.strip_prefix("due:").or_else(|| word.strip_prefix('@')) {
                (false, val)
            } else {
                (false, "")
            };

            if !clean_word.is_empty() {
                // Check "next friday"
                if clean_word == "next" && i + 1 < words.len() {
                    let next_str = words[i + 1].2.as_str();
                    let is_weekday = parse_weekday_code(next_str).is_some();
                    if is_date_unit_full(next_str) || is_weekday {
                        matched_kind = Some(if is_start {
                            SyntaxType::StartDate
                        } else {
                            SyntaxType::DueDate
                        });
                        words_consumed = 2;
                        // Peek ahead for time (e.g. @next friday 2pm)
                        if i + 2 < words.len() && is_time_format(&words[i + 2].2) {
                            words_consumed += 1;
                        }
                    }
                }
                // Check "in 2 days"
                else if clean_word == "in" && i + 1 < words.len() {
                    let next_token_str = words[i + 1].2.as_str();
                    let next_next = if i + 2 < words.len() {
                        Some(words[i + 2].2.as_str())
                    } else {
                        None
                    };

                    if let Some((_, _, consumed)) =
                        parse_amount_and_unit(next_token_str, next_next, false)
                    {
                        matched_kind = Some(if is_start {
                            SyntaxType::StartDate
                        } else {
                            SyntaxType::DueDate
                        });
                        words_consumed = 1 + 1 + consumed;
                        // Peek ahead for time
                        if i + words_consumed < words.len()
                            && is_time_format(&words[i + words_consumed].2)
                        {
                            words_consumed += 1;
                        }
                    }
                }
                // Check standard dates (@tomorrow, @2025-01-01)
                else {
                    // Just basic check if parser accepts it
                    if parse_smart_date(clean_word).is_some()
                        || parse_weekday_code(clean_word).is_some()
                        || parse_time_string(clean_word).is_some()
                    {
                        matched_kind = Some(if is_start {
                            SyntaxType::StartDate
                        } else {
                            SyntaxType::DueDate
                        });
                        // Peek ahead for time
                        if i + 1 < words.len() && is_time_format(&words[i + 1].2) {
                            words_consumed = 2;
                        }
                    }
                }
            }
        }

        // 3. Reminders (rem:10m, rem:in 5m, rem:next friday)
        if matched_kind.is_none() && word_lower.starts_with("rem:") {
            matched_kind = Some(SyntaxType::Reminder);

            let clean_val = if word.len() > 4 { &word[4..] } else { "" };

            // Helper to skip whitespace and get next non-whitespace token index
            let find_next_token = |start_idx: usize| -> Option<usize> {
                (start_idx..words.len()).find(|&idx| !words[idx].2.trim().is_empty())
            };

            // CASE A: "rem:in 5m" or "rem: in 5m"
            if clean_val.eq_ignore_ascii_case("in")
                || (clean_val.is_empty()
                    && find_next_token(i + 1)
                        .map(|idx| words[idx].2.eq_ignore_ascii_case("in"))
                        .unwrap_or(false))
            {
                // If "rem:" was separate from "in", consume "in"
                let mut offset = 0;
                if clean_val.is_empty() {
                    offset = 1;
                }

                if let Some(next_idx) = find_next_token(i + 1 + offset) {
                    let next_token_str = words[next_idx].2.as_str();
                    let next_next_idx = find_next_token(next_idx + 1);
                    let next_next = next_next_idx.map(|idx| words[idx].2.as_str());

                    if let Some((_, _, consumed)) =
                        parse_amount_and_unit(next_token_str, next_next, false)
                    {
                        let last_idx = if consumed > 0 {
                            next_next_idx.unwrap_or(next_idx)
                        } else {
                            next_idx
                        };
                        words_consumed = last_idx - i + 1;
                    }
                }
            }
            // CASE B: "rem:next friday" or "rem: next friday"
            else if clean_val.eq_ignore_ascii_case("next")
                || (clean_val.is_empty()
                    && find_next_token(i + 1)
                        .map(|idx| words[idx].2.eq_ignore_ascii_case("next"))
                        .unwrap_or(false))
            {
                let mut offset = 0;
                if clean_val.is_empty() {
                    offset = 1;
                }

                if let Some(next_idx) = find_next_token(i + 1 + offset) {
                    let next_word = &words[next_idx].2;
                    // Check if next word is a weekday or unit
                    if parse_weekday_code(next_word).is_some() || is_date_unit_full(next_word) {
                        words_consumed = next_idx - i + 1;

                        // Look ahead for time (e.g., rem:next friday 9am)
                        if let Some(time_idx) = find_next_token(next_idx + 1)
                            && is_time_format(&words[time_idx].2)
                        {
                            words_consumed = time_idx - i + 1;
                        }
                    }
                }
            }
            // CASE C: "rem:tomorrow", "rem:2025...", "rem:monday"
            else if !clean_val.is_empty()
                && (parse_smart_date(clean_val).is_some() || parse_next_date(clean_val).is_some())
            {
                // Look ahead for time (rem:tomorrow 16:00)
                if let Some(next_idx) = find_next_token(i + 1)
                    && is_time_format(&words[next_idx].2)
                {
                    words_consumed = next_idx - i + 1;
                }
            }
            // CASE D: Space after colon (rem: tomorrow)
            else if clean_val.is_empty()
                && let Some(next_idx) = find_next_token(i + 1)
            {
                let next_word = &words[next_idx].2;
                if parse_smart_date(next_word).is_some() || parse_next_date(next_word).is_some() {
                    words_consumed = next_idx - i + 1;
                    if let Some(time_idx) = find_next_token(next_idx + 1)
                        && is_time_format(&words[time_idx].2)
                    {
                        words_consumed = time_idx - i + 1;
                    }
                } else if parse_duration(next_word).is_some() || is_time_format(next_word) {
                    words_consumed = next_idx - i + 1;
                }
            }
        }

        // 4. Single tokens
        if matched_kind.is_none() {
            if word.starts_with("@@") || word_lower.starts_with("loc:") {
                matched_kind = Some(SyntaxType::Location);
            } else if word_lower.starts_with("url:")
                || (word.starts_with("[[") && word.ends_with("]]"))
            {
                matched_kind = Some(SyntaxType::Url);
            } else if word == "+cal" || word == "-cal" {
                matched_kind = Some(SyntaxType::Calendar);
            } else if word == "+pin" || word == "-pin" {
                matched_kind = Some(SyntaxType::Pin);
            } else if word_lower.starts_with("geo:") {
                let mut temp_consumed = 1;
                let mut raw_val = word[4..].to_string();
                let is_geo_word = |w: &str| -> bool {
                    let upper = w.to_uppercase();
                    if matches!(
                        upper.as_str(),
                        "N" | "S" | "E" | "W" | "N," | "S," | "E," | "W,"
                    ) {
                        return true;
                    }
                    if !w.chars().any(|c| c.is_ascii_digit()) {
                        return false;
                    }
                    w.chars()
                        .all(|c| c.is_ascii_digit() || " .,-°NSEWnsew".contains(c))
                };

                while i + temp_consumed < words.len() {
                    let next_word = &words[i + temp_consumed].2;
                    if is_geo_word(next_word) {
                        raw_val.push(' ');
                        raw_val.push_str(next_word);
                        temp_consumed += 1;
                    } else {
                        break;
                    }
                }

                if is_valid_geo(&raw_val) {
                    matched_kind = Some(SyntaxType::Geo);
                    words_consumed = temp_consumed;
                }
            } else if word_lower.starts_with("desc:") {
                matched_kind = Some(SyntaxType::Description);
            } else if word.starts_with('!') && word.len() > 1 && word[1..].parse::<u8>().is_ok() {
                matched_kind = Some(SyntaxType::Priority);
            } else if word.starts_with('~') || word_lower.starts_with("est:") {
                matched_kind = Some(SyntaxType::Duration);
            } else if word_lower.starts_with("spent:") {
                // New logic for spent:
                matched_kind = Some(SyntaxType::Duration);
            } else if word_lower.starts_with("done:") {
                // Highlight done: tokens like dates (reuse DueDate highlighting/color)
                matched_kind = Some(SyntaxType::DueDate);
            } else if word.starts_with('#') {
                matched_kind = Some(SyntaxType::Tag);
            }
        }

        if let Some(kind) = matched_kind {
            let final_end = words[i + words_consumed - 1].1;
            tokens.push(SyntaxToken {
                kind,
                start: *start,
                end: final_end,
            });
            cursor = final_end;
            i += words_consumed;
        } else {
            tokens.push(SyntaxToken {
                kind: SyntaxType::Text,
                start: *start,
                end: *end,
            });
            cursor = *end;
            i += 1;
        }
    }
    if cursor < input.len() {
        tokens.push(SyntaxToken {
            kind: SyntaxType::Text,
            start: cursor,
            end: input.len(),
        });
    }
    tokens
}

fn unescape(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    let mut chars = s.chars();
    while let Some(c) = chars.next() {
        if c == '\\' {
            if let Some(next) = chars.next() {
                out.push(next);
            } else {
                out.push('\\');
            }
        } else {
            out.push(c);
        }
    }
    out
}

pub fn strip_quotes(s: &str) -> String {
    let s = s.trim();
    if s.len() >= 2
        && ((s.starts_with('"') && s.ends_with('"')) || (s.starts_with('{') && s.ends_with('}')))
    {
        unescape(&s[1..s.len() - 1])
    } else {
        unescape(s)
    }
}

pub fn quote_value(s: &str) -> String {
    if s.contains(' ') || s.contains('"') || s.contains('\\') || s.contains('#') || s.is_empty() {
        let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
        format!("\"{}\"", escaped)
    } else {
        s.to_string()
    }
}

fn split_input_respecting_quotes(input: &str) -> Vec<(usize, usize, String)> {
    let mut parts = Vec::new();
    let mut current = String::new();
    let mut start_idx = 0;
    let mut in_quote = false;
    let mut brace_depth: usize = 0;
    let mut escaped = false;
    let chars = input.char_indices().peekable();

    for (idx, c) in chars {
        if current.is_empty() && !in_quote && brace_depth == 0 && !c.is_whitespace() {
            start_idx = idx;
        }
        if escaped {
            current.push(c);
            escaped = false;
            continue;
        }
        match c {
            '\\' => {
                escaped = true;
                current.push('\\');
            }
            '"' if brace_depth == 0 => {
                in_quote = !in_quote;
                current.push(c);
            }
            '{' if !in_quote => {
                brace_depth += 1;
                current.push(c);
            }
            '}' if !in_quote => {
                brace_depth = brace_depth.saturating_sub(1);
                current.push(c);
            }
            ws if ws.is_whitespace() && !in_quote && brace_depth == 0 => {
                if !current.is_empty() {
                    parts.push((start_idx, idx, current.clone()));
                    current.clear();
                }
            }
            _ => current.push(c),
        }
    }
    if !current.is_empty() {
        parts.push((start_idx, input.len(), current));
    }
    parts
}

pub fn expand_braces(input: &str) -> Vec<String> {
    let mut results = Vec::new();
    expand_braces_recursive("", input.trim(), &mut results);
    results
}

fn split_by_comma_at_depth_zero(input: &str) -> Vec<&str> {
    let mut parts = Vec::new();
    let mut depth = 0;
    let mut last_idx = 0;
    let mut in_quote = false;
    let mut escaped = false;
    for (i, c) in input.char_indices() {
        if escaped {
            escaped = false;
            continue;
        }
        if c == '\\' {
            escaped = true;
            continue;
        }
        if c == '"' {
            in_quote = !in_quote;
        }
        if !in_quote {
            if c == '{' {
                depth += 1;
            } else if c == '}' {
                if depth > 0 {
                    depth -= 1;
                }
            } else if c == ',' && depth == 0 {
                parts.push(&input[last_idx..i]);
                last_idx = i + c.len_utf8();
            }
        }
    }
    parts.push(&input[last_idx..]);
    parts
}

fn expand_braces_recursive(prefix: &str, input: &str, results: &mut Vec<String>) {
    let parts = split_by_comma_at_depth_zero(input);
    for part in parts {
        let part = part.trim();
        if part.is_empty() {
            continue;
        }

        let mut brace_start = None;
        let mut depth = 0;
        let mut in_quote = false;
        let mut escaped = false;
        for (i, c) in part.char_indices() {
            if escaped {
                escaped = false;
                continue;
            }
            if c == '\\' {
                escaped = true;
                continue;
            }
            if c == '"' {
                in_quote = !in_quote;
            }
            if !in_quote {
                if c == '{' {
                    if depth == 0 {
                        brace_start = Some(i);
                        break;
                    }
                    depth += 1;
                } else if c == '}' && depth > 0 {
                    depth -= 1;
                }
            }
        }

        if let Some(start) = brace_start
            && part.ends_with('}')
        {
            let base = part[..start].trim();
            let inner = &part[start + 1..part.len() - 1];

            let new_prefix = if base.is_empty() {
                prefix.to_string()
            } else {
                let sep = if base.ends_with('=')
                    || base.ends_with(':')
                    || prefix.is_empty()
                    || prefix.ends_with(':')
                {
                    ""
                } else {
                    ":"
                };
                format!("{}{}{}", prefix, sep, base)
            };

            let pass_prefix = if new_prefix.is_empty() {
                String::new()
            } else if new_prefix.ends_with('=') || new_prefix.ends_with(':') {
                new_prefix
            } else {
                format!("{}:", new_prefix)
            };

            expand_braces_recursive(&pass_prefix, inner, results);
            continue;
        }

        results.push(format!("{}{}", prefix, part));
    }
}

fn collect_alias_expansions(
    token: &str,
    aliases: &HashMap<String, Vec<String>>,
    visited: &mut HashSet<String>,
    depth: usize,
) -> Vec<String> {
    // Hard limit to prevent stack overflows from manually corrupted config files
    if depth > 10 {
        return Vec::new();
    }

    let mut results = Vec::new();
    let mut search_key: Option<String> = None;

    // 1. Tag Lookup
    if token.starts_with('#') {
        search_key = Some(strip_quotes(token.trim_start_matches('#')));
    }
    // 2. Location Lookup
    else if token.starts_with("@@") || token.to_lowercase().starts_with("loc:") {
        let raw = if token.starts_with("@@") {
            token.trim_start_matches("@@")
        } else {
            &token[4..]
        };
        let clean = strip_quotes(raw);
        if !clean.is_empty() {
            search_key = Some(format!("@@{}", clean));
        }
    }

    if let Some(key) = search_key {
        let mut search = key.as_str();
        let mut found_values = None;
        let mut matched_key = String::new();

        loop {
            if let Some(vals) = aliases.get(search) {
                found_values = Some(vals);
                matched_key = search.to_string();
                break;
            }

            // Handle hierarchy (tag:subtag or @@loc:subloc)
            if let Some(idx) = search.rfind(':') {
                if idx > 0 {
                    search = &search[..idx];
                } else {
                    break;
                }
            } else {
                break;
            }
        }

        if let Some(values) = found_values {
            if visited.contains(&matched_key) {
                return results;
            }
            visited.insert(matched_key);
            for val in values {
                let child_expansions = collect_alias_expansions(val, aliases, visited, depth + 1);
                results.extend(child_expansions);
                results.push(val.clone());
            }
        }
    }
    results
}

// --- DATE PARSING HELPERS ---

fn is_date_unit_full(s: &str) -> bool {
    let lower = s.to_lowercase();
    matches!(
        lower.as_str(),
        "day" | "days" | "week" | "weeks" | "month" | "months" | "year" | "years"
    )
}
fn is_date_unit_short(s: &str) -> bool {
    let lower = s.to_lowercase();
    matches!(
        lower.as_str(),
        "m" | "min"
            | "minute"
            | "minutes"
            | "h"
            | "hour"
            | "hours"
            | "d"
            | "day"
            | "days"
            | "w"
            | "week"
            | "weeks"
            | "mo"
            | "month"
            | "months"
            | "y"
            | "year"
            | "years"
    )
}
fn parse_amount_and_unit(
    first: &str,
    second: Option<&str>,
    strict_unit: bool,
) -> Option<(u32, String, usize)> {
    if let Some(next_token) = second
        && let Some(amt) = parse_english_number(first)
    {
        let unit = next_token.to_lowercase();
        let is_valid = if strict_unit {
            is_date_unit_full(&unit)
        } else {
            is_date_unit_short(&unit)
        };
        if is_valid {
            return Some((amt, unit, 1));
        }
    }
    if !strict_unit {
        let lower = first.to_lowercase();
        let (amt_str, unit_str) = if let Some(idx) = lower.find(|c: char| !c.is_numeric()) {
            lower.split_at(idx)
        } else {
            return None;
        };
        if let Ok(amt) = amt_str.parse::<u32>()
            && is_date_unit_short(unit_str)
        {
            return Some((amt, unit_str.to_string(), 0));
        }
    }
    None
}
fn parse_english_number(s: &str) -> Option<u32> {
    match s.to_lowercase().as_str() {
        "one" | "1" => Some(1),
        "two" | "2" => Some(2),
        "three" | "3" => Some(3),
        "four" | "4" => Some(4),
        "five" | "5" => Some(5),
        "six" | "6" => Some(6),
        "seven" | "7" => Some(7),
        "eight" | "8" => Some(8),
        "nine" | "9" => Some(9),
        "ten" | "10" => Some(10),
        "eleven" | "11" => Some(11),
        "twelve" | "12" => Some(12),
        _ => s.parse::<u32>().ok(),
    }
}
fn parse_freq_from_unit(u: &str) -> &'static str {
    let s = u.to_lowercase();
    if s.starts_with('d') {
        "DAILY"
    } else if s.starts_with('w') {
        "WEEKLY"
    } else if s.starts_with("mo") {
        "MONTHLY"
    } else if s.starts_with('y') {
        "YEARLY"
    } else {
        ""
    }
}

fn code_to_full_day(code: &str) -> &'static str {
    match code {
        "MO" => "monday",
        "TU" => "tuesday",
        "WE" => "wednesday",
        "TH" => "thursday",
        "FR" => "friday",
        "SA" => "saturday",
        "SU" => "sunday",
        _ => "",
    }
}

fn month_num_to_short_name(num: u32) -> &'static str {
    match num {
        1 => "jan",
        2 => "feb",
        3 => "mar",
        4 => "apr",
        5 => "may",
        6 => "jun",
        7 => "jul",
        8 => "aug",
        9 => "sep",
        10 => "oct",
        11 => "nov",
        12 => "dec",
        _ => "",
    }
}

pub fn prettify_recurrence(rrule: &str, is_relative: bool) -> String {
    let mut freq = "";
    let mut interval = "";
    let mut byday = "";
    let mut bymonth = "";
    let mut until = "";

    // Parse RRULE components
    for part in rrule.split(';') {
        if let Some(v) = part.strip_prefix("FREQ=") {
            freq = v;
        } else if let Some(v) = part.strip_prefix("INTERVAL=") {
            interval = v;
        } else if let Some(v) = part.strip_prefix("BYDAY=") {
            byday = v;
        } else if let Some(v) = part.strip_prefix("BYMONTH=") {
            bymonth = v;
        } else if let Some(v) = part.strip_prefix("UNTIL=") {
            until = v;
        }
    }

    // Format UNTIL if present
    let mut until_str = String::new();
    if !until.is_empty() {
        let date_part = until.split('T').next().unwrap_or(until);
        if date_part.len() >= 8 {
            until_str = format!(
                " until {}-{}-{}",
                &date_part[0..4],
                &date_part[4..6],
                &date_part[6..8]
            );
        } else {
            until_str = format!(" until {}", until);
        }
    }

    // 1. Handle Weekday Logic (Inclusions vs Exclusions)
    // Only apply if interval is 1 (standard) or empty
    if freq == "WEEKLY" && !byday.is_empty() && (interval.is_empty() || interval == "1") {
        let days: Vec<&str> = byday.split(',').collect();
        let all_codes = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];

        // A. Single Day -> @every monday or @after monday
        if days.len() == 1 && bymonth.is_empty() {
            let d_name = code_to_full_day(days[0]);
            if !d_name.is_empty() {
                let prefix = if is_relative { "@after" } else { "@every" };
                return format!("{} {}{}", prefix, d_name, until_str);
            }
        }

        // B. 2-3 Days -> @every monday,tuesday,wednesday or @after monday,tuesday,wednesday
        if days.len() >= 2 && days.len() <= 3 && bymonth.is_empty() {
            let day_names: Vec<String> = days
                .iter()
                .map(|code| code_to_full_day(code).to_string())
                .filter(|name| !name.is_empty())
                .collect();

            if day_names.len() == days.len() {
                let prefix = if is_relative { "@after" } else { "@every" };
                return format!("{} {}{}", prefix, day_names.join(","), until_str);
            }
        }

        // C. Majority Days (4+) -> @daily except ...
        // If we have >= 4 days, listing exclusions is cleaner than listing inclusions
        if days.len() >= 4 {
            let missing_days: Vec<String> = all_codes
                .iter()
                .filter(|code| !days.contains(code))
                .map(|code| code_to_full_day(code).to_string())
                .collect();

            // Check if we also have month exclusions to combine
            if !bymonth.is_empty() {
                let months: Vec<u32> = bymonth
                    .split(',')
                    .filter_map(|m| m.parse::<u32>().ok())
                    .collect();

                let missing_months: Vec<String> = (1..=12)
                    .filter(|m| !months.contains(m))
                    .map(|m| month_num_to_short_name(m).to_string())
                    .collect();

                if !missing_months.is_empty() {
                    // Combine both day and month exclusions
                    let mut combined = missing_days;
                    combined.extend(missing_months);
                    return format!("@daily{} except {}", until_str, combined.join(","));
                }
            }

            if missing_days.is_empty() {
                // All days present -> @daily (or check month logic if bymonth present)
                if bymonth.is_empty() {
                    return format!("@daily{}", until_str);
                }
                // If bymonth is present, fall through to month logic section
            } else {
                // @daily except x,y
                return format!("@daily{} except {}", until_str, missing_days.join(","));
            }
        }
    }

    // 2. Handle Month Logic (Exclusions only)
    // Show except format whenever BYMONTH is present (more user-friendly than raw RRULE)
    // Works for DAILY, WEEKLY, MONTHLY, and YEARLY frequencies
    if !bymonth.is_empty() && (interval.is_empty() || interval == "1") {
        let months: Vec<u32> = bymonth
            .split(',')
            .filter_map(|m| m.parse::<u32>().ok())
            .collect();

        let missing: Vec<String> = (1..=12)
            .filter(|m| !months.contains(m))
            .map(|m| month_num_to_short_name(m).to_string())
            .collect();

        if missing.is_empty() {
            // All 12 months present -> use base frequency without except
            // Continue to section 4 to handle the base frequency
        } else {
            // 1-11 months excluded -> @<freq> except x,y,z
            let base = match freq {
                "DAILY" => "@daily",
                "WEEKLY" => "@weekly",
                "MONTHLY" => "@monthly",
                "YEARLY" => "@yearly",
                _ => "",
            };
            if !base.is_empty() {
                return format!("{}{} except {}", base, until_str, missing.join(","));
            }
        }
    }

    // 3. Handle Custom Intervals (e.g. @every 3 days or @after 3d)
    if !freq.is_empty() && !interval.is_empty() && interval != "1" {
        let unit = match freq {
            "DAILY" => {
                if is_relative {
                    "d"
                } else {
                    "days"
                }
            }
            "WEEKLY" => {
                if is_relative {
                    "w"
                } else {
                    "weeks"
                }
            }
            "MONTHLY" => {
                if is_relative {
                    "mo"
                } else {
                    "months"
                }
            }
            "YEARLY" => {
                if is_relative {
                    "y"
                } else {
                    "years"
                }
            }
            _ => "",
        };
        if !unit.is_empty() {
            if is_relative {
                return format!("@after {}{}{}", interval, unit, until_str);
            } else {
                return format!("@every {} {}{}", interval, unit, until_str);
            }
        }
    }

    // 4. Handle Standard Presets (@daily, @monthly... or @after 1d, @after 1w)
    if !freq.is_empty() {
        if is_relative {
            let unit = match freq {
                "DAILY" => "1d",
                "WEEKLY" => "1w",
                "MONTHLY" => "1mo",
                "YEARLY" => "1y",
                _ => "",
            };
            if !unit.is_empty() {
                return format!("@after {}{}", unit, until_str);
            }
        } else {
            let base = match freq {
                "DAILY" => "@daily",
                "WEEKLY" => "@weekly",
                "MONTHLY" => "@monthly",
                "YEARLY" => "@yearly",
                _ => "",
            };
            if !base.is_empty() {
                return format!("{}{}", base, until_str);
            }
        }
    }

    // 5. Fallback to raw string
    format!("rec:{}", rrule)
}

fn parse_single_duration(val: &str) -> Option<u32> {
    let lower = val.to_lowercase();
    if let Some(n) = lower.strip_suffix("min") {
        return n.parse::<u32>().ok();
    }
    if let Some(n) = lower.strip_suffix('m') {
        return n.parse::<u32>().ok();
    } else if let Some(n) = lower.strip_suffix('h') {
        return n.parse::<u32>().ok().map(|h| h * 60);
    } else if let Some(n) = lower.strip_suffix('d') {
        return n.parse::<u32>().ok().map(|d| d * 24 * 60);
    } else if let Some(n) = lower.strip_suffix('w') {
        return n.parse::<u32>().ok().map(|w| w * 7 * 24 * 60);
    } else if let Some(n) = lower.strip_suffix("mo") {
        return n.parse::<u32>().ok().map(|mo| mo * 30 * 24 * 60);
    } else if let Some(n) = lower.strip_suffix('y') {
        return n.parse::<u32>().ok().map(|y| y * 365 * 24 * 60);
    }
    None
}

pub fn parse_duration(val: &str) -> Option<u32> {
    parse_single_duration(val)
}

pub fn parse_duration_range(val: &str) -> Option<(u32, Option<u32>)> {
    if let Some((left, right)) = val.split_once('-') {
        let min = parse_single_duration(left)?;
        let max = parse_single_duration(right)?;
        if max >= min {
            return Some((min, Some(max)));
        }
        return Some((min, None));
    }
    let single = parse_single_duration(val)?;
    Some((single, None))
}

pub fn format_duration_compact(mins: u32) -> String {
    if mins == 0 {
        return "".to_string();
    }
    if mins.is_multiple_of(525600) {
        format!("{}y", mins / 525600)
    } else if mins.is_multiple_of(43200) {
        format!("{}mo", mins / 43200)
    } else if mins.is_multiple_of(10080) {
        format!("{}w", mins / 10080)
    } else if mins.is_multiple_of(1440) {
        format!("{}d", mins / 1440)
    } else if mins.is_multiple_of(60) {
        format!("{}h", mins / 60)
    } else {
        format!("{}m", mins)
    }
}

fn parse_recurrence(val: &str) -> Option<String> {
    match val.to_lowercase().as_str() {
        "daily" => Some("FREQ=DAILY".to_string()),
        "weekly" => Some("FREQ=WEEKLY".to_string()),
        "monthly" => Some("FREQ=MONTHLY".to_string()),
        "yearly" => Some("FREQ=YEARLY".to_string()),
        _ => {
            if val.to_uppercase().starts_with("FREQ=") {
                Some(val.to_string())
            } else {
                None
            }
        }
    }
}

fn parse_smart_date(val: &str) -> Option<DateType> {
    // 1. Standard ISO YYYY-MM-DD
    if let Ok(date) = NaiveDate::parse_from_str(val, "%Y-%m-%d") {
        return Some(DateType::AllDay(date));
    }
    // 2. Basic ISO YYYYMMDD
    if val.len() == 8
        && val.chars().all(|c| c.is_numeric())
        && let Ok(date) = NaiveDate::parse_from_str(val, "%Y%m%d")
    {
        return Some(DateType::AllDay(date));
    }
    // 3. Abbreviated YYYY-MM
    if val.len() == 7 && val.chars().nth(4) == Some('-') {
        let y = val[0..4].parse::<i32>().ok();
        let m = val[5..7].parse::<u32>().ok();
        if let (Some(year), Some(month)) = (y, m)
            && (1..=12).contains(&month)
        {
            return Some(DateType::Month(year, month));
        }
    }
    // 4. Year only YYYY
    if val.len() == 4
        && val.chars().all(|c| c.is_numeric())
        && let Ok(y) = val.parse::<i32>()
    {
        return Some(DateType::Year(y));
    }

    // Existing natural language logic...
    let now = Local::now().date_naive();
    let lower = val.to_lowercase();
    if lower == "today" {
        return Some(DateType::AllDay(now));
    }
    if lower == "tomorrow" {
        return Some(DateType::AllDay(now + Duration::days(1)));
    }
    if let Some(n) = lower.strip_suffix('d').and_then(|s| s.parse::<i64>().ok()) {
        return Some(DateType::AllDay(now + Duration::days(n)));
    }
    if let Some(n) = lower.strip_suffix('w').and_then(|s| s.parse::<i64>().ok()) {
        return Some(DateType::AllDay(now + Duration::days(n * 7)));
    }
    if let Some(n) = lower.strip_suffix("mo").and_then(|s| s.parse::<i64>().ok()) {
        return Some(DateType::AllDay(now + Duration::days(n * 30)));
    }
    if let Some(n) = lower.strip_suffix('y').and_then(|s| s.parse::<i64>().ok()) {
        return Some(DateType::AllDay(now + Duration::days(n * 365)));
    }
    None
}

fn parse_weekday_code(s: &str) -> Option<&'static str> {
    match s.to_lowercase().as_str() {
        "mo" | "mon" | "monday" | "mondays" => Some("MO"),
        "tu" | "tue" | "tuesday" | "tuesdays" => Some("TU"),
        "we" | "wed" | "wednesday" | "wednesdays" => Some("WE"),
        "th" | "thu" | "thursday" | "thursdays" => Some("TH"),
        "fr" | "fri" | "friday" | "fridays" => Some("FR"),
        "sa" | "sat" | "saturday" | "saturdays" => Some("SA"),
        "su" | "sun" | "sunday" | "sundays" => Some("SU"),
        _ => None,
    }
}

fn parse_month_code(s: &str) -> Option<u32> {
    match s.to_lowercase().as_str() {
        "jan" | "january" => Some(1),
        "feb" | "february" => Some(2),
        "mar" | "march" => Some(3),
        "apr" | "april" => Some(4),
        "may" => Some(5),
        "jun" | "june" => Some(6),
        "jul" | "july" => Some(7),
        "aug" | "august" => Some(8),
        "sep" | "sept" | "september" => Some(9),
        "oct" | "october" => Some(10),
        "nov" | "november" => Some(11),
        "dec" | "december" => Some(12),
        _ => None,
    }
}

fn parse_weekday_date(s: &str) -> Option<NaiveDate> {
    parse_next_date(s)
}

fn parse_next_date(unit: &str) -> Option<NaiveDate> {
    let now = Local::now().date_naive();
    match unit.to_lowercase().as_str() {
        "week" => Some(now + Duration::days(7)),
        "month" => Some(now + Duration::days(30)),
        "year" => Some(now + Duration::days(365)),
        _ => {
            if let Some(code) = parse_weekday_code(unit) {
                let target = match code {
                    "MO" => chrono::Weekday::Mon,
                    "TU" => chrono::Weekday::Tue,
                    "WE" => chrono::Weekday::Wed,
                    "TH" => chrono::Weekday::Thu,
                    "FR" => chrono::Weekday::Fri,
                    "SA" => chrono::Weekday::Sat,
                    "SU" => chrono::Weekday::Sun,
                    _ => return None,
                };
                return next_weekday(now, target);
            }
            None
        }
    }
}

fn parse_in_date(amount: u32, unit: &str) -> Option<NaiveDate> {
    let now = Local::now().date_naive();
    let days = match unit.to_lowercase().as_str() {
        "d" | "day" | "days" => amount as i64,
        "w" | "week" | "weeks" => amount as i64 * 7,
        "mo" | "month" | "months" => amount as i64 * 30,
        "y" | "year" | "years" => amount as i64 * 365,
        _ => return None,
    };
    Some(now + Duration::days(days))
}

fn next_weekday(from: NaiveDate, target: chrono::Weekday) -> Option<NaiveDate> {
    let mut d = from + Duration::days(1);
    while d.weekday() != target {
        d += Duration::days(1);
    }
    Some(d)
}

fn calculate_first_occurrence(rrule: &str, today: NaiveDate) -> NaiveDate {
    // Parse RRULE to determine first occurrence date
    let mut freq = "";
    let mut byday = "";
    let mut bymonth = "";

    for part in rrule.split(';') {
        if let Some(v) = part.strip_prefix("FREQ=") {
            freq = v;
        } else if let Some(v) = part.strip_prefix("BYDAY=") {
            byday = v;
        } else if let Some(v) = part.strip_prefix("BYMONTH=") {
            bymonth = v;
        }
    }

    // For WEEKLY with BYDAY, find the next occurrence of any listed day
    if freq == "WEEKLY" && !byday.is_empty() {
        let days: Vec<&str> = byday.split(',').collect();

        // Parse BYMONTH constraint if present
        let allowed_months: Vec<u32> = if !bymonth.is_empty() {
            bymonth
                .split(',')
                .filter_map(|m| m.parse::<u32>().ok())
                .collect()
        } else {
            vec![]
        };

        // Helper to check if a date is in an allowed month
        let is_month_allowed = |date: NaiveDate| -> bool {
            allowed_months.is_empty() || allowed_months.contains(&date.month())
        };

        let mut earliest: Option<NaiveDate> = None;

        for day_code in days {
            let weekday = match day_code {
                "MO" => chrono::Weekday::Mon,
                "TU" => chrono::Weekday::Tue,
                "WE" => chrono::Weekday::Wed,
                "TH" => chrono::Weekday::Thu,
                "FR" => chrono::Weekday::Fri,
                "SA" => chrono::Weekday::Sat,
                "SU" => chrono::Weekday::Sun,
                _ => continue,
            };

            // Check if today matches (both weekday AND month if BYMONTH is present)
            if today.weekday() == weekday && is_month_allowed(today) {
                return today;
            }

            // Otherwise find next occurrence of this weekday that's in an allowed month
            let mut candidate = today;
            for _ in 0..60 {
                // Check up to ~8 weeks ahead to find valid occurrence
                if let Some(next) = next_weekday(candidate, weekday) {
                    if is_month_allowed(next) {
                        if earliest.is_none() || next < earliest.unwrap() {
                            earliest = Some(next);
                        }
                        break;
                    }
                    candidate = next;
                } else {
                    break;
                }
            }
        }

        if let Some(date) = earliest {
            return date;
        }
    }

    // For MONTHLY with BYMONTH, find the next occurrence in an allowed month
    if (freq == "MONTHLY" || freq == "DAILY" || freq == "WEEKLY") && !bymonth.is_empty() {
        let allowed_months: Vec<u32> = bymonth
            .split(',')
            .filter_map(|m| m.parse::<u32>().ok())
            .collect();

        if !allowed_months.is_empty() {
            let current_month = today.month();

            // Check if current month is allowed
            if allowed_months.contains(&current_month) {
                return today;
            }

            // Find next allowed month
            let mut check_date = today;
            for _ in 0..12 {
                // Move to first day of next month
                if check_date.month() == 12 {
                    check_date = NaiveDate::from_ymd_opt(check_date.year() + 1, 1, 1).unwrap();
                } else {
                    check_date =
                        NaiveDate::from_ymd_opt(check_date.year(), check_date.month() + 1, 1)
                            .unwrap();
                }

                if allowed_months.contains(&check_date.month()) {
                    return check_date;
                }
            }
        }
    }

    // For DAILY or other patterns, default to today
    today
}

fn finalize_date_token(
    d: DateType,
    stream: &[String],
    next_idx: usize,
    consumed: &mut usize,
) -> (DateType, Option<DateType>) {
    match d {
        DateType::AllDay(nd) => {
            // Only try to merge a time token if it's a specific day
            if next_idx < stream.len() {
                let next_token = &stream[next_idx];
                if let Some((t1, t2)) = parse_time_range(next_token) {
                    *consumed += 1;
                    let mut end_nd = nd;
                    if t2 < t1 {
                        end_nd += Duration::days(1);
                    }
                    return (
                        DateType::Specific(crate::model::item::safe_local_to_utc(nd, t1)),
                        Some(DateType::Specific(crate::model::item::safe_local_to_utc(
                            end_nd, t2,
                        ))),
                    );
                } else if let Some(t) = parse_time_string(next_token) {
                    *consumed += 1;
                    return (
                        DateType::Specific(crate::model::item::safe_local_to_utc(nd, t)),
                        None,
                    );
                }
            }
            (DateType::AllDay(nd), None)
        }
        // Months and Years don't support specific times (14:00)
        _ => (d, None),
    }
}

pub fn normalize_geo(val: String) -> String {
    if val.eq_ignore_ascii_case("here") {
        return val;
    }
    if !val.contains('°')
        && !val.contains('N')
        && !val.contains('S')
        && !val.contains('E')
        && !val.contains('W')
        && !val.contains('n')
        && !val.contains('s')
        && !val.contains('e')
        && !val.contains('w')
    {
        return val.replace(' ', "");
    }
    let clean = val.replace(['°', ' '], "");
    let mut parts = clean.split(',');
    if let (Some(lat_str), Some(lon_str)) = (parts.next(), parts.next()) {
        let parse_part = |s: &str| -> String {
            let mut num = String::new();
            let mut sign = "";
            for c in s.chars() {
                if c == 'S' || c == 's' || c == 'W' || c == 'w' {
                    sign = "-";
                } else if c.is_numeric() || c == '.' || c == '-' {
                    num.push(c);
                }
            }
            format!("{}{}", sign, num)
        };
        return format!("{},{}", parse_part(lat_str), parse_part(lon_str));
    }
    val.replace(' ', "")
}

pub fn escape_summary(summary: &str) -> String {
    let mut escaped_words = Vec::new();
    for word in summary.split_whitespace() {
        if is_special_token(word) {
            escaped_words.push(format!("\\{}", word));
        } else {
            escaped_words.push(word.to_string());
        }
    }
    escaped_words.join(" ")
}

fn is_special_token(word: &str) -> bool {
    let lower = word.to_lowercase();
    if word.starts_with('@')
        || word.starts_with('#')
        || word.starts_with('!')
        || word.starts_with('^')
        || word.starts_with('~')
    {
        return true;
    }
    if lower.starts_with("loc:")
        || lower.starts_with("url:")
        || lower.starts_with("geo:")
        || lower.starts_with("desc:")
        || lower.starts_with("due:")
        || lower.starts_with("start:")
        || lower.starts_with("rec:")
        || lower.starts_with("est:")
        || lower.starts_with("rem:")
        || lower.starts_with("spent:")
        || lower.starts_with("until")
        || lower.starts_with("except")
        || lower == "+pin"
        || lower == "-pin"
    {
        return true;
    }
    false
}

pub fn apply_smart_input(
    task: &mut Task,
    input: &str,
    aliases: &HashMap<String, Vec<String>>,
    default_reminder_time: Option<NaiveTime>,
) {
    let mut summary_words = Vec::new();
    // Reset fields
    task.unmapped_properties
        .retain(|p| p.key != "X-CFAIT-RECUR-FROM-COMPLETION");
    task.priority = 0;
    task.due = None;
    task.dtstart = None;
    task.rrule = None;
    task.estimated_duration = None;
    task.estimated_duration_max = None;
    task.location = None;
    task.url = None;
    task.geo = None;
    task.categories.clear();
    task.alarms.clear();
    task.exdates.clear();
    task.percent_complete = None; // Reset percentage so edits apply cleanly

    // Added enum to defer alarm processing until dates are resolved
    enum PendingAlarm {
        Relative(u32),
        TimeOnly(NaiveTime),
        Absolute(DateTime<Utc>),
    }
    let mut pending_alarms = Vec::new();

    let user_tokens: Vec<String> = split_input_respecting_quotes(input)
        .into_iter()
        .map(|(_, _, s)| s)
        .collect();

    let mut background_tokens = Vec::new();
    let mut visited = HashSet::new();

    for token in &user_tokens {
        let expanded = collect_alias_expansions(token, aliases, &mut visited, 0);
        background_tokens.extend(expanded);
    }

    let mut stream = background_tokens;
    stream.extend(user_tokens);

    let mut blocked_weekdays = HashSet::new();
    let mut blocked_months = HashSet::new();
    let mut has_recurrence = false; // Track if we've seen a recurrence token

    let mut i = 0;
    while i < stream.len() {
        let token = &stream[i];
        let mut consumed = 1;
        let token_lower = token.to_lowercase();

        // 1. Recurrence
        if (token == "rec:every" || token == "@every" || token == "@after") && i + 1 < stream.len()
        {
            let next_token_str = stream[i + 1].as_str();
            let next_next = if i + 2 < stream.len() {
                Some(stream[i + 2].as_str())
            } else {
                None
            };
            if let Some((interval, unit, extra_consumed)) =
                parse_amount_and_unit(next_token_str, next_next, false)
            {
                let freq = parse_freq_from_unit(&unit);
                if !freq.is_empty() {
                    task.rrule = Some(format!("FREQ={};INTERVAL={}", freq, interval));
                    if token == "@after"
                        && !task
                            .unmapped_properties
                            .iter()
                            .any(|p| p.key == "X-CFAIT-RECUR-FROM-COMPLETION")
                    {
                        task.unmapped_properties.push(crate::model::RawProperty {
                            key: "X-CFAIT-RECUR-FROM-COMPLETION".to_string(),
                            value: "TRUE".to_string(),
                            params: vec![],
                        });
                    }
                    has_recurrence = true;
                    consumed = 1 + 1 + extra_consumed;
                } else {
                    summary_words.push(unescape(token));
                }
            } else {
                // Try parsing as comma-separated weekdays
                let parts: Vec<&str> = next_token_str.split(',').map(|s| s.trim()).collect();
                let weekday_codes: Vec<String> = parts
                    .iter()
                    .filter_map(|part| parse_weekday_code(part).map(|s| s.to_string()))
                    .collect();

                if !weekday_codes.is_empty() && weekday_codes.len() == parts.len() {
                    // All parts were valid weekdays
                    task.rrule = Some(format!("FREQ=WEEKLY;BYDAY={}", weekday_codes.join(",")));
                    if token == "@after"
                        && !task
                            .unmapped_properties
                            .iter()
                            .any(|p| p.key == "X-CFAIT-RECUR-FROM-COMPLETION")
                    {
                        task.unmapped_properties.push(crate::model::RawProperty {
                            key: "X-CFAIT-RECUR-FROM-COMPLETION".to_string(),
                            value: "TRUE".to_string(),
                            params: vec![],
                        });
                    }
                    has_recurrence = true;
                    consumed = 2;
                } else {
                    summary_words.push(unescape(token));
                }
            }

            if let Some(rrule_str) = task.rrule.clone() {
                // Check for time (e.g. "@every sunday 8pm")
                if i + consumed < stream.len() {
                    let potential_time = &stream[i + consumed];
                    if let Some(t) = parse_time_string(potential_time) {
                        // Found a time! Calculate the dates immediately.
                        let today = Local::now().date_naive();
                        let first_date = calculate_first_occurrence(&rrule_str, today);

                        let dt_specific = crate::model::item::safe_local_to_utc(first_date, t);

                        let date_val = DateType::Specific(dt_specific);
                        task.due = Some(date_val.clone());
                        task.dtstart = Some(date_val);

                        consumed += 1;
                    }
                }
            }
        } else if let Some(rrule) = token
            .strip_prefix("rec:")
            .and_then(parse_recurrence)
            .or_else(|| token.strip_prefix('@').and_then(parse_recurrence))
            .or_else(|| {
                if token_lower.starts_with("freq=") {
                    Some(token.to_string()) // Preserve original case
                } else {
                    None
                }
            })
        {
            task.rrule = Some(rrule.clone());
            has_recurrence = true;

            // Check for time (e.g. "@daily 8pm")
            if i + consumed < stream.len() {
                let potential_time = &stream[i + consumed];
                if let Some(t) = parse_time_string(potential_time) {
                    let today = Local::now().date_naive();
                    let first_date = calculate_first_occurrence(&rrule, today);
                    let dt_specific = crate::model::item::safe_local_to_utc(first_date, t);
                    let date_val = DateType::Specific(dt_specific);
                    task.due = Some(date_val.clone());
                    task.dtstart = Some(date_val);
                    consumed += 1;
                }
            }
        }
        // Handle "until <date>" - only if recurrence is already defined
        else if has_recurrence && token_lower == "until" && i + 1 < stream.len() {
            // Parse date
            let next_token = &stream[i + 1];
            if let Some(d) = parse_smart_date(next_token) {
                // Append UNTIL=... to rrule.
                if let Some(mut rr) = task.rrule.take() {
                    // Remove existing UNTIL if any
                    if !rr.contains("UNTIL=") {
                        let date_str = match d {
                            DateType::AllDay(nd) => nd.format("%Y%m%d").to_string(),
                            DateType::Specific(dt) => dt.format("%Y%m%d").to_string(),
                            DateType::Month(y, m) => format!("{:04}{:02}01", y, m),
                            DateType::Year(y) => format!("{:04}0101", y),
                        };
                        rr.push_str(&format!(";UNTIL={}", date_str));
                    }
                    task.rrule = Some(rr);
                }
                consumed = 2;
            } else {
                summary_words.push(unescape(token));
            }
        }
        // Handle "except <list>" - only if recurrence is already defined
        else if has_recurrence && token_lower == "except" && i + 1 < stream.len() {
            let next_token = &stream[i + 1];
            let parts: Vec<&str> = next_token.split(',').map(|s| s.trim()).collect();

            let mut matched_any = false;
            // Case 1: Single item, which might be a date followed by a time
            if parts.len() == 1 {
                let part = parts[0];
                let mut temp_consumed = 1; // Base consumption for the date/weekday/month token

                if let Some(d) = parse_smart_date(part) {
                    let (dt, _) = finalize_date_token(d, &stream, i + 2, &mut temp_consumed);
                    task.exdates.push(dt);
                    matched_any = true;
                } else if let Some(code) = parse_weekday_code(part) {
                    blocked_weekdays.insert(code.to_string());
                    matched_any = true;
                } else if let Some(m) = parse_month_code(part) {
                    blocked_months.insert(m);
                    matched_any = true;
                }

                if matched_any {
                    consumed = 1 + temp_consumed; // 1 for 'except' + temp_consumed for the values
                } else {
                    // Debug: print the part that failed to parse
                    eprintln!(
                        "DEBUG: Failed to parse part '{}' as date/weekday/month",
                        part
                    );
                }
            } else {
                // Case 2: Comma-separated list (must be simple AllDay/Weekday/Month)
                for part in parts {
                    if let Some(d) = parse_smart_date(part) {
                        // Convert fuzzy dates to AllDay for exdates
                        let all_day_date = match d {
                            DateType::AllDay(nd) => DateType::AllDay(nd),
                            DateType::Specific(dt) => DateType::AllDay(dt.date_naive()),
                            DateType::Month(y, m) => {
                                DateType::AllDay(NaiveDate::from_ymd_opt(y, m, 1).unwrap())
                            }
                            DateType::Year(y) => {
                                DateType::AllDay(NaiveDate::from_ymd_opt(y, 1, 1).unwrap())
                            }
                        };
                        task.exdates.push(all_day_date); // Comma lists are always AllDay
                        matched_any = true;
                    } else if let Some(code) = parse_weekday_code(part) {
                        blocked_weekdays.insert(code.to_string());
                        matched_any = true;
                    } else if let Some(m) = parse_month_code(part) {
                        blocked_months.insert(m);
                        matched_any = true;
                    }
                }
                if matched_any {
                    consumed = 2;
                }
            }

            if !matched_any {
                summary_words.push(unescape(token));
            }
        }
        // 2. Reminders (rem:)
        else if let Some(val) = token_lower.strip_prefix("rem:") {
            let clean_val = if val.is_empty() && i + 1 < stream.len() {
                // Handle "rem: 10m" (space after colon)
                consumed += 1;
                &stream[i + 1]
            } else {
                // Handle "rem:10m"
                if token.len() > 4 { &token[4..] } else { "" }
            };

            // Handle "rem:in 5m" or "rem: in 5m"
            if clean_val.eq_ignore_ascii_case("in") && i + consumed < stream.len() {
                let next_str = &stream[i + consumed];
                let next_next = if i + consumed + 1 < stream.len() {
                    Some(stream[i + consumed + 1].as_str())
                } else {
                    None
                };

                // Reuse existing duration parser helper
                if let Some((amt, unit, extra)) = parse_amount_and_unit(next_str, next_next, false)
                {
                    // 1. Calculate Duration in minutes
                    let mins = match unit.as_str() {
                        "d" | "day" | "days" => amt * 1440,
                        "w" | "week" | "weeks" => amt * 10080,
                        "h" | "hour" | "hours" => amt * 60,
                        _ => amt, // assume minutes for m/min or bare numbers
                    };

                    // 2. Create ABSOLUTE alarm (Now + Duration) - this is different from rem:5m
                    let now = Local::now();
                    let target = now + Duration::minutes(mins as i64);
                    let target_utc = target.with_timezone(&Utc);

                    pending_alarms.push(PendingAlarm::Absolute(target_utc));

                    consumed += 1 + extra; // Consume amount + unit tokens
                } else {
                    summary_words.push(unescape(token));
                }
            }
            // Handle "rem:next friday" or "rem: next friday"
            else if clean_val.eq_ignore_ascii_case("next") && i + consumed < stream.len() {
                let next_str = &stream[i + consumed];
                if let Some(target_date) = parse_next_date(next_str) {
                    consumed += 1;

                    // Look ahead for time (e.g. rem:next friday 2pm)
                    let mut time = default_reminder_time
                        .unwrap_or_else(|| NaiveTime::from_hms_opt(9, 0, 0).unwrap());
                    if i + consumed < stream.len()
                        && let Some(t) = parse_time_string(&stream[i + consumed])
                    {
                        time = t;
                        consumed += 1;
                    }

                    let local_dt = crate::model::item::safe_local_to_utc(target_date, time);

                    pending_alarms.push(PendingAlarm::Absolute(local_dt));
                } else {
                    summary_words.push(unescape(token));
                }
            } else if !clean_val.is_empty() {
                // A. Duration (rem:10m)
                if let Some(d) = parse_duration(clean_val) {
                    pending_alarms.push(PendingAlarm::Relative(d));
                }
                // B. Time Only (rem:8pm) -> Store for later contextual resolution
                else if let Some(t) = parse_time_string(clean_val) {
                    pending_alarms.push(PendingAlarm::TimeOnly(t));
                }
                // C. Date + Optional Time (rem:2025-12-27 12:30, rem:tomorrow 9am, rem:monday 9am)
                else if let Some(d) = parse_smart_date(clean_val)
                    .or_else(|| parse_next_date(clean_val).map(DateType::AllDay))
                {
                    // Look ahead for time
                    let mut time_part = None;

                    if i + consumed < stream.len() {
                        let potential_time = &stream[i + consumed];
                        if let Some(t) = parse_time_string(potential_time) {
                            time_part = Some(t);
                            consumed += 1;
                        }
                    }

                    // USE CONFIG OR DEFAULT TO 08:00
                    let fallback = default_reminder_time
                        .unwrap_or_else(|| NaiveTime::from_hms_opt(8, 0, 0).unwrap());
                    let t = time_part.unwrap_or(fallback);

                    let dt = match d {
                        DateType::AllDay(nd) => crate::model::item::safe_local_to_utc(nd, t),
                        DateType::Specific(dt) => dt,
                        DateType::Month(y, m) => crate::model::item::safe_local_to_utc(
                            NaiveDate::from_ymd_opt(y, m, 1).unwrap(),
                            t,
                        ),
                        DateType::Year(y) => crate::model::item::safe_local_to_utc(
                            NaiveDate::from_ymd_opt(y, 1, 1).unwrap(),
                            t,
                        ),
                    };
                    pending_alarms.push(PendingAlarm::Absolute(dt));
                } else {
                    summary_words.push(unescape(token));
                }
            } else {
                summary_words.push(unescape(token));
            }
        }
        // 3. New Fields (@@, loc:, url:, geo:, desc:, !, ~, spent:, done:)
        else if let Some(_val) = token_lower.strip_prefix("done:") {
            let clean_val = if token.len() > 5 { &token[5..] } else { "" };
            let mut matched = false;

            if !clean_val.is_empty() {
                // Support Percentage Completion (e.g. done:25%)
                if clean_val.ends_with('%') {
                    if let Ok(pc) = clean_val.trim_end_matches('%').parse::<u8>() {
                        task.percent_complete = Some(pc.min(100));
                        consumed = 1;
                        matched = true;
                    }
                }
                // Fallback to Date Completion
                else if let Ok(ndt) =
                    chrono::NaiveDateTime::parse_from_str(clean_val, "%Y-%m-%d %H:%M")
                {
                    let utc_dt = crate::model::item::safe_local_to_utc(ndt.date(), ndt.time());
                    task.set_completion_date(Some(utc_dt));
                    consumed = 1;
                    matched = true;
                } else if let Some(d) = parse_smart_date(clean_val) {
                    let mut temp_consumed = 1;
                    let (dt, _) =
                        finalize_date_token(d, &stream, i + temp_consumed, &mut temp_consumed);

                    let utc_dt = match dt {
                        DateType::Specific(t) => t,
                        DateType::AllDay(d) => crate::model::item::safe_local_to_utc(
                            d,
                            chrono::NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
                        ),
                        DateType::Month(y, m) => {
                            let d = NaiveDate::from_ymd_opt(y, m, 1).unwrap();
                            crate::model::item::safe_local_to_utc(
                                d,
                                chrono::NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
                            )
                        }
                        DateType::Year(y) => {
                            let d = NaiveDate::from_ymd_opt(y, 1, 1).unwrap();
                            crate::model::item::safe_local_to_utc(
                                d,
                                chrono::NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
                            )
                        }
                    };

                    task.set_completion_date(Some(utc_dt));
                    consumed = temp_consumed;
                    matched = true;
                }
            }

            if !matched {
                summary_words.push(unescape(token));
            }
        } else if let Some(val) = token_lower.strip_prefix("spent:") {
            // Allow formats like `spent:30m` or `spent:1h`
            if let Some(mins) = parse_duration(val) {
                task.time_spent_seconds = (mins as u64) * 60;
            } else {
                summary_words.push(unescape(token));
            }
        } else if token.starts_with("@@") {
            let is_triple = token.starts_with("@@@");
            let val = strip_quotes(token.trim_start_matches('@'));
            if val.is_empty() {
                summary_words.push(unescape(token));
            } else {
                task.location = Some(val.clone());
                if is_triple {
                    summary_words.push(val);
                }
            }
        } else if token_lower.starts_with("loc:") {
            let val = strip_quotes(&token[4..]);
            if val.is_empty() {
                summary_words.push(unescape(token));
            } else {
                task.location = Some(val);
            }
        } else if token_lower.starts_with("url:") {
            task.url = Some(strip_quotes(&token[4..]));
        } else if token.starts_with("[[") && token.ends_with("]]") {
            task.url = Some(token[2..token.len() - 2].to_string());
        } else if token == "+cal" {
            task.create_event = Some(true);
        } else if token == "-cal" {
            task.create_event = Some(false);
        } else if token == "+pin" {
            task.pinned = true;
        } else if token == "-pin" {
            task.pinned = false;
        } else if token_lower.starts_with("geo:") {
            let mut raw_val = token[4..].to_string();
            let mut temp_consumed = 1;
            let is_geo_word = |w: &str| -> bool {
                let upper = w.to_uppercase();
                if matches!(
                    upper.as_str(),
                    "N" | "S" | "E" | "W" | "N," | "S," | "E," | "W,"
                ) {
                    return true;
                }
                if !w.chars().any(|c| c.is_ascii_digit()) {
                    return false;
                }
                w.chars()
                    .all(|c| c.is_ascii_digit() || " .,-°NSEWnsew".contains(c))
            };

            while i + temp_consumed < stream.len() {
                let next_token = &stream[i + temp_consumed];
                if is_geo_word(next_token) {
                    raw_val.push(' ');
                    raw_val.push_str(next_token);
                    temp_consumed += 1;
                } else {
                    break;
                }
            }

            if is_valid_geo(&raw_val) {
                task.geo = Some(normalize_geo(strip_quotes(&raw_val)));
                consumed = temp_consumed;
            } else {
                summary_words.push(unescape(token));
            }
        } else if token_lower.starts_with("desc:") {
            let desc_val = strip_quotes(&token[5..]);
            if task.description.is_empty() {
                task.description = desc_val;
            } else {
                task.description.push_str(&format!("\n{}", desc_val));
            }
        } else if token.starts_with('#') {
            let is_double = token.starts_with("##");
            let cat_expr = token.trim_start_matches('#');
            if !cat_expr.is_empty() {
                let expanded_cats = expand_braces(cat_expr);
                for cat in expanded_cats {
                    let clean_cat = strip_quotes(&cat);
                    if !clean_cat.is_empty() && !task.categories.contains(&clean_cat) {
                        task.categories.push(clean_cat);
                    }
                }
                if is_double {
                    summary_words.push(strip_quotes(cat_expr));
                }
            }
        } else if token.starts_with('!') && token.len() > 1 {
            if let Ok(p) = token[1..].parse::<u8>() {
                // FIX: Clamp priority to 0-9 range (RFC 5545)
                task.priority = p.min(9);
            } else {
                summary_words.push(unescape(token));
            }
        } else if token.starts_with('~') || token_lower.starts_with("est:") {
            let val = strip_quotes(if let Some(s) = token.strip_prefix('~') {
                s
            } else {
                &token[4..]
            });
            if let Some((min, max_opt)) = parse_duration_range(&val) {
                task.estimated_duration = Some(min);
                task.estimated_duration_max = max_opt;
            } else {
                summary_words.push(unescape(token));
            }
        }
        // 4. Dates (Multi-word + Time)
        else if (token.starts_with('@')
            || token.starts_with('^')
            || token_lower.starts_with("due:")
            || token_lower.starts_with("start:"))
            && stream.get(i + 1).is_some()
        {
            // Determine if setting start, due, or both
            let (set_start, set_due, clean) = if let Some(s) = token.strip_prefix("^@") {
                (true, true, s)
            } else if let Some(_v) = token
                .strip_prefix('^')
                .or_else(|| token_lower.strip_prefix("start:"))
            {
                let clean_v = if let Some(s) = token.strip_prefix('^') {
                    s
                } else {
                    &token[6..]
                };
                (true, false, clean_v)
            } else {
                let clean_v = if let Some(s) = token.strip_prefix('@') {
                    s
                } else if token_lower.starts_with("due:") {
                    &token[4..]
                } else {
                    ""
                };
                (false, true, clean_v)
            };

            let mut matched_date = false;
            // "next friday"
            if clean == "next" && stream.get(i + 1).is_some() {
                let next_str = &stream[i + 1];
                if let Some(d) = parse_next_date(next_str) {
                    let mut temp_consumed = 2; // "next" + unit
                    let (dt, dt_end) = finalize_date_token(
                        DateType::AllDay(d),
                        &stream,
                        i + temp_consumed,
                        &mut temp_consumed,
                    );
                    if let Some(end) = dt_end {
                        task.dtstart = Some(dt);
                        task.due = Some(end);
                    } else {
                        if set_start {
                            task.dtstart = Some(dt.clone());
                        }
                        if set_due {
                            task.due = Some(dt);
                        }
                    }
                    consumed = temp_consumed;
                    matched_date = true;
                }
            }
            // "in 2 days"
            else if clean == "in" && i + 1 < stream.len() {
                let next_token_str = stream[i + 1].as_str();
                let next_next = if i + 2 < stream.len() {
                    Some(stream[i + 2].as_str())
                } else {
                    None
                };
                if let Some((amount, unit, extra)) =
                    parse_amount_and_unit(next_token_str, next_next, false)
                    && let Some(d) = parse_in_date(amount, &unit)
                {
                    let mut temp_consumed = 1 + 1 + extra;
                    let (dt, dt_end) = finalize_date_token(
                        DateType::AllDay(d),
                        &stream,
                        i + temp_consumed,
                        &mut temp_consumed,
                    );
                    if let Some(end) = dt_end {
                        task.dtstart = Some(dt);
                        task.due = Some(end);
                    } else {
                        if set_start {
                            task.dtstart = Some(dt.clone());
                        }
                        if set_due {
                            task.due = Some(dt);
                        }
                    }
                    consumed = temp_consumed;
                    matched_date = true;
                }
            }

            if !matched_date {
                if let Some(d) = parse_smart_date(clean) {
                    let mut temp_consumed = 1;
                    let (dt, dt_end) =
                        finalize_date_token(d, &stream, i + temp_consumed, &mut temp_consumed);
                    if let Some(end) = dt_end {
                        task.dtstart = Some(dt);
                        task.due = Some(end);
                    } else {
                        if set_start {
                            task.dtstart = Some(dt.clone());
                        }
                        if set_due {
                            task.due = Some(dt);
                        }
                    }
                    consumed = temp_consumed;
                } else if let Some((t1, t2)) = parse_time_range(clean) {
                    let now_local = Local::now();
                    let mut target_date = now_local.date_naive();
                    if t1 <= now_local.time() {
                        target_date += Duration::days(1);
                    }
                    let dt1 =
                        DateType::Specific(crate::model::item::safe_local_to_utc(target_date, t1));

                    let mut target_date_end = target_date;
                    if t2 < t1 {
                        target_date_end += Duration::days(1);
                    }
                    let dt2 = DateType::Specific(crate::model::item::safe_local_to_utc(
                        target_date_end,
                        t2,
                    ));

                    task.dtstart = Some(dt1);
                    task.due = Some(dt2);
                } else if let Some(t) = parse_time_string(clean) {
                    let now_local = Local::now();
                    let mut target_date = now_local.date_naive();
                    if t <= now_local.time() {
                        target_date += Duration::days(1);
                    }
                    let dt = crate::model::item::safe_local_to_utc(target_date, t);
                    let dt_type = DateType::Specific(dt);
                    if set_start {
                        task.dtstart = Some(dt_type.clone());
                    }
                    if set_due {
                        task.due = Some(dt_type);
                    }
                } else if let Some(d) = parse_weekday_date(clean) {
                    let mut temp_consumed = 1;
                    let (dt, dt_end) = finalize_date_token(
                        DateType::AllDay(d),
                        &stream,
                        i + temp_consumed,
                        &mut temp_consumed,
                    );
                    if let Some(end) = dt_end {
                        task.dtstart = Some(dt);
                        task.due = Some(end);
                    } else {
                        if set_start {
                            task.dtstart = Some(dt.clone());
                        }
                        if set_due {
                            task.due = Some(dt);
                        }
                    }
                    consumed = temp_consumed;
                } else if let Some(rrule) = parse_recurrence(clean) {
                    task.rrule = Some(rrule);
                    has_recurrence = true;
                } else {
                    summary_words.push(unescape(token));
                }
            }
        }
        // 5. Single Word Dates
        else if let Some(val) = token
            .strip_prefix("rec:")
            .or_else(|| token.strip_prefix('@'))
            // Check for ^@ prefix here too
            .or_else(|| token.strip_prefix("^@"))
        {
            // Detect if this specific token was ^@ to determine assignment mode
            let (set_start, set_due) = if token.starts_with("^@") {
                (true, true)
            } else {
                (false, true) // Default for @ or rec: logic below (rec logic overrides this anyway)
            };

            // Recurrence, Date, or Weekday
            if let Some(rrule) = parse_recurrence(val) {
                task.rrule = Some(rrule.clone());
                has_recurrence = true;

                // Look ahead for time (e.g. "@daily 09:00")
                if i + consumed < stream.len() {
                    let potential_time = &stream[i + consumed];
                    if let Some(t) = parse_time_string(potential_time) {
                        let today = Local::now().date_naive();
                        let first_date = calculate_first_occurrence(&rrule, today);

                        let dt_specific = crate::model::item::safe_local_to_utc(first_date, t);

                        let date_val = DateType::Specific(dt_specific);
                        if set_start {
                            task.dtstart = Some(date_val.clone());
                        }
                        if set_due {
                            task.due = Some(date_val.clone());
                        }
                        // Defaulting: ensure dtstart/due are set appropriately when not explicitly requested
                        if !set_start {
                            task.dtstart = Some(date_val.clone());
                        }
                        if !set_due {
                            task.due = Some(date_val.clone());
                        }
                        consumed += 1;
                    }
                }
            } else if token.starts_with("rec:") {
                if let Some((interval, unit, _)) = parse_amount_and_unit(val, None, false) {
                    let freq = parse_freq_from_unit(&unit);
                    if !freq.is_empty() {
                        let rrule_str = format!("FREQ={};INTERVAL={}", freq, interval);
                        task.rrule = Some(rrule_str.clone());
                        has_recurrence = true;

                        // Check for time (e.g. "rec:2d 09:00")
                        if i + consumed < stream.len() {
                            let potential_time = &stream[i + consumed];
                            if let Some(t) = parse_time_string(potential_time) {
                                let today = Local::now().date_naive();
                                let first_date = calculate_first_occurrence(&rrule_str, today);

                                let dt_specific =
                                    crate::model::item::safe_local_to_utc(first_date, t);

                                let date_val = DateType::Specific(dt_specific);
                                task.due = Some(date_val.clone());
                                task.dtstart = Some(date_val);
                                consumed += 1;
                            }
                        }
                    } else {
                        summary_words.push(unescape(token));
                    }
                } else {
                    summary_words.push(unescape(token));
                }
            } else if let Some(d) = parse_smart_date(val) {
                let mut temp_consumed = 1;
                let (dt, dt_end) =
                    finalize_date_token(d, &stream, i + temp_consumed, &mut temp_consumed);
                if let Some(end) = dt_end {
                    task.dtstart = Some(dt);
                    task.due = Some(end);
                } else {
                    if set_start {
                        task.dtstart = Some(dt.clone());
                    }
                    if set_due {
                        task.due = Some(dt);
                    }
                }
                consumed = temp_consumed;
            } else if let Some((t1, t2)) = parse_time_range(val) {
                let now_local = Local::now();
                let mut target_date = now_local.date_naive();
                if t1 <= now_local.time() {
                    target_date += Duration::days(1);
                }
                let dt1 =
                    DateType::Specific(crate::model::item::safe_local_to_utc(target_date, t1));

                let mut target_date_end = target_date;
                if t2 < t1 {
                    target_date_end += Duration::days(1);
                }
                let dt2 =
                    DateType::Specific(crate::model::item::safe_local_to_utc(target_date_end, t2));

                task.dtstart = Some(dt1);
                task.due = Some(dt2);
            } else if let Some(rrule) = parse_recurrence(val) {
                task.rrule = Some(rrule);
                has_recurrence = true;
            } else if let Some(t) = parse_time_string(val) {
                let now_local = Local::now();
                let mut target_date = now_local.date_naive();
                if t <= now_local.time() {
                    target_date += Duration::days(1);
                }
                let dt = crate::model::item::safe_local_to_utc(target_date, t);
                let dt_type = DateType::Specific(dt);
                if set_start {
                    task.dtstart = Some(dt_type.clone());
                }
                if set_due {
                    task.due = Some(dt_type);
                }
            } else if let Some(d) = parse_weekday_date(val) {
                let mut temp_consumed = 1;
                let (dt, dt_end) = finalize_date_token(
                    DateType::AllDay(d),
                    &stream,
                    i + temp_consumed,
                    &mut temp_consumed,
                );
                if let Some(end) = dt_end {
                    task.dtstart = Some(dt);
                    task.due = Some(end);
                } else {
                    if set_start {
                        task.dtstart = Some(dt.clone());
                    }
                    if set_due {
                        task.due = Some(dt);
                    }
                }
                consumed = temp_consumed;
            } else if let Some((t1, t2)) = parse_time_range(val) {
                let now_local = Local::now();
                let mut target_date = now_local.date_naive();
                if t1 <= now_local.time() {
                    target_date += Duration::days(1);
                }
                let dt1 =
                    DateType::Specific(crate::model::item::safe_local_to_utc(target_date, t1));

                let mut target_date_end = target_date;
                if t2 < t1 {
                    target_date_end += Duration::days(1);
                }
                let dt2 =
                    DateType::Specific(crate::model::item::safe_local_to_utc(target_date_end, t2));

                task.dtstart = Some(dt1);
                task.due = Some(dt2);
            } else if let Some(_stripped) = token_lower.strip_prefix("due:") {
                let real_val = &token[4..];
                if let Some(d) = parse_smart_date(real_val) {
                    let mut temp_consumed = 1;
                    let (dt, _) =
                        finalize_date_token(d, &stream, i + temp_consumed, &mut temp_consumed);
                    task.due = Some(dt);
                    consumed = temp_consumed;
                } else {
                    summary_words.push(unescape(token));
                }
            } else {
                summary_words.push(unescape(token));
            }
        } else if token_lower.starts_with("due:") {
            let val = &token[4..];
            if let Some(d) = parse_smart_date(val) {
                let mut temp_consumed = 1;
                let (dt, _) =
                    finalize_date_token(d, &stream, i + temp_consumed, &mut temp_consumed);
                task.due = Some(dt);
                consumed = temp_consumed;
            } else {
                summary_words.push(unescape(token));
            }
        } else if let Some(_val) = token
            .strip_prefix('^')
            .or_else(|| token_lower.strip_prefix("start:"))
        {
            let clean_val = if let Some(s) = token.strip_prefix('^') {
                s
            } else {
                &token[6..]
            };
            if let Some(d) = parse_smart_date(clean_val) {
                let mut temp_consumed = 1;
                let (dt, _) =
                    finalize_date_token(d, &stream, i + temp_consumed, &mut temp_consumed);
                task.dtstart = Some(dt);
                consumed = temp_consumed;
            } else if let Some(d) = parse_weekday_date(clean_val) {
                let mut temp_consumed = 1;
                let (dt, _) = finalize_date_token(
                    DateType::AllDay(d),
                    &stream,
                    i + temp_consumed,
                    &mut temp_consumed,
                );
                task.dtstart = Some(dt);
                consumed = temp_consumed;
            } else {
                summary_words.push(unescape(token));
            }
        } else {
            summary_words.push(unescape(token));
        }
        i += consumed;
    }

    // Apply accumulated rule modifications
    if (!blocked_weekdays.is_empty() || !blocked_months.is_empty())
        && let Some(mut rrule) = task.rrule.take()
    {
        // Apply blocked weekdays (convert to inclusion)
        if !blocked_weekdays.is_empty() {
            // If FREQ=DAILY, construct WEEKLY logic
            if rrule.contains("FREQ=DAILY") {
                rrule = rrule.replace("FREQ=DAILY", "FREQ=WEEKLY");
            }

            // If we already have BYDAY (unlikely for simple presets), do nothing to avoid conflict
            if !rrule.contains("BYDAY=") {
                let all_days = vec!["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
                let allowed: Vec<&str> = all_days
                    .into_iter()
                    .filter(|d| !blocked_weekdays.contains(*d))
                    .collect();
                if !allowed.is_empty() {
                    rrule.push_str(&format!(";BYDAY={}", allowed.join(",")));
                }
            }
        }

        // Apply blocked months (convert to inclusion)
        if !blocked_months.is_empty() && !rrule.contains("BYMONTH=") {
            let allowed: Vec<String> = (1..=12)
                .filter(|m| !blocked_months.contains(m))
                .map(|m| m.to_string())
                .collect();
            if !allowed.is_empty() {
                rrule.push_str(&format!(";BYMONTH={}", allowed.join(",")));
            }
        }
        task.rrule = Some(rrule);
    }

    task.summary = summary_words.join(" ");
    task.categories.sort();
    task.categories.dedup();

    // If recurrence is set but no date is specified, auto-set both start and due to first occurrence
    // This makes recurring tasks more intuitive: "@daily" means "due today, and every day after"
    if let Some(ref rrule) = task.rrule
        && task.dtstart.is_none()
        && task.due.is_none()
    {
        let today = Local::now().date_naive();
        let first_occurrence = calculate_first_occurrence(rrule, today);
        task.dtstart = Some(DateType::AllDay(first_occurrence));
        task.due = Some(DateType::AllDay(first_occurrence));
    }

    // Ensure start date cannot be strictly after due date
    if let (Some(start), Some(due)) = (&task.dtstart, &task.due)
        && start.to_start_comparison_time() > due.to_comparison_time()
    {
        // Clamp start to due to avoid invalid ranges where start > due.
        task.dtstart = Some(due.clone());
    }

    // MAP PENDING ALARMS TO TASK DATES
    for pending in pending_alarms {
        match pending {
            PendingAlarm::Relative(mins) => {
                task.alarms.push(Alarm::new_relative(mins));
            }
            PendingAlarm::Absolute(dt) => {
                task.alarms.push(Alarm::new_absolute(dt));
            }
            PendingAlarm::TimeOnly(t) => {
                let anchor_date = task
                    .due
                    .as_ref()
                    .or(task.dtstart.as_ref())
                    .map(|d| d.to_date_naive());
                if let Some(target_date) = anchor_date {
                    let dt = crate::model::item::safe_local_to_utc(target_date, t);
                    task.alarms.push(Alarm::new_absolute(dt));
                } else {
                    let now_local = Local::now();
                    let mut target_date = now_local.date_naive();
                    if t <= now_local.time() {
                        target_date += Duration::days(1);
                    }
                    let dt = crate::model::item::safe_local_to_utc(target_date, t);
                    task.alarms.push(Alarm::new_absolute(dt));
                }
            }
        }
    }
}

pub fn parse_session_input(input: &str) -> Option<crate::model::item::WorkSession> {
    use chrono::Local;
    let now = Local::now();
    let mut target_date = now.date_naive();

    let tokens: Vec<&str> = input.split_whitespace().collect();
    if tokens.is_empty() {
        return None;
    }

    let mut duration_mins = None;
    let mut start_time = None;
    let mut end_time = None;

    for token in tokens {
        let lower = token.to_lowercase();
        if lower == "today" {
            target_date = now.date_naive();
        } else if lower == "yesterday" {
            target_date = now.date_naive() - chrono::Duration::days(1);
        } else if let Ok(d) = chrono::NaiveDate::parse_from_str(&lower, "%Y-%m-%d") {
            target_date = d;
        } else if lower.contains('-') && lower.split('-').count() == 2 {
            let parts: Vec<&str> = lower.split('-').collect();
            if let (Some(t1), Some(t2)) = (parse_time_string(parts[0]), parse_time_string(parts[1]))
            {
                start_time = Some(t1);
                end_time = Some(t2);
            }
        } else if let Some(mins) = parse_duration(&lower) {
            duration_mins = Some(mins);
        } else if let Some(t) = parse_time_string(&lower) {
            start_time = Some(t);
        }
    }

    if let (Some(s), Some(e)) = (start_time, end_time) {
        let start_dt = crate::model::item::safe_local_to_utc(target_date, s);
        let mut end_target_date = target_date;
        if e < s {
            end_target_date += chrono::Duration::days(1);
        }
        let end_dt = crate::model::item::safe_local_to_utc(end_target_date, e);
        return Some(crate::model::item::WorkSession {
            start: start_dt.timestamp(),
            end: end_dt.timestamp(),
        });
    }

    if let Some(dur) = duration_mins {
        if let Some(s) = start_time {
            let start_dt = crate::model::item::safe_local_to_utc(target_date, s);
            let end_dt = start_dt + chrono::Duration::minutes(dur as i64);
            return Some(crate::model::item::WorkSession {
                start: start_dt.timestamp(),
                end: end_dt.timestamp(),
            });
        } else if target_date == now.date_naive() {
            let end_dt = now.with_timezone(&chrono::Utc);
            let start_dt = end_dt - chrono::Duration::minutes(dur as i64);
            return Some(crate::model::item::WorkSession {
                start: start_dt.timestamp(),
                end: end_dt.timestamp(),
            });
        } else {
            let s = chrono::NaiveTime::from_hms_opt(12, 0, 0).unwrap();
            let start_dt = crate::model::item::safe_local_to_utc(target_date, s);
            let end_dt = start_dt + chrono::Duration::minutes(dur as i64);
            return Some(crate::model::item::WorkSession {
                start: start_dt.timestamp(),
                end: end_dt.timestamp(),
            });
        }
    }

    None
}
