use anyhow::{Context, Result};
use caldir_core::caldir::Caldir;
use caldir_core::calendar::Calendar;
use caldir_core::event::{Event, EventTime, Reminder};
use chrono::Duration;
use dialoguer::{Input, Select};
use owo_colors::OwoColorize;

#[allow(clippy::too_many_arguments)]
pub fn run(
    title: Option<String>,
    start: Option<String>,
    end: Option<String>,
    duration: Option<String>,
    location: Option<String>,
    calendar_slug: Option<String>,
    reminder_args: Vec<String>,
    no_reminders: bool,
    calendars: Vec<Calendar>,
) -> Result<()> {
    let interactive = title.is_none() || start.is_none();

    // --- Title ---
    let title = match title {
        Some(t) => t,
        None => Input::<String>::new()
            .with_prompt("  Title")
            .interact_text()?,
    };

    // --- Start ---
    let start_time = if let Some(s) = start {
        parse_datetime(&s)?
    } else {
        prompt_with_retry("  When?", parse_datetime)?
    };

    // --- Duration / End ---
    let is_allday = matches!(start_time, EventTime::Date(_));
    let default_hint = if is_allday { "1 day" } else { "1 hour" };

    let end_time = if let Some(end_input) = end {
        let parsed = parse_datetime(&end_input)?;
        check_end_matches_start(&start_time, &parsed)?;
        parsed
    } else if let Some(dur_input) = duration {
        apply_duration(&start_time, &dur_input)?
    } else if interactive {
        prompt_duration(&start_time, default_hint)?
    } else {
        default_end(&start_time)
    };

    // --- Location ---
    let location = if let Some(loc) = location {
        if loc.is_empty() { None } else { Some(loc) }
    } else if interactive {
        let loc: String = Input::new()
            .with_prompt("  Where? (skip)")
            .default(String::new())
            .show_default(false)
            .interact_text()?;
        if loc.is_empty() { None } else { Some(loc) }
    } else {
        None
    };

    // --- Reminders ---
    let reminders: Vec<Reminder> = if no_reminders {
        vec![]
    } else if !reminder_args.is_empty() {
        reminder_args
            .iter()
            .map(|r| parse_reminder(r))
            .collect::<Result<_>>()?
    } else {
        let caldir = Caldir::load()?;
        caldir
            .config()
            .parse_default_reminders()
            .map_err(|e| anyhow::anyhow!("{}", e))?
            .unwrap_or_default()
    };

    // --- Calendar ---
    let calendar = resolve_calendar(calendar_slug, &calendars, interactive)?;

    let event = Event::new(title, start_time, end_time, None, location, None, reminders);

    let path = calendar.create_event(&event)?;

    let display_path = if let Ok(home) = std::env::var("HOME") {
        if let Ok(relative) = path.strip_prefix(&home) {
            format!("~/{}", relative.display())
        } else {
            path.display().to_string()
        }
    } else {
        path.display().to_string()
    };

    if interactive {
        println!();
    }
    println!("{}", format!("  Created: {}", display_path).green());

    Ok(())
}

/// Prompt the user with retry on parse errors.
fn prompt_with_retry<F>(prompt: &str, parse: F) -> Result<EventTime>
where
    F: Fn(&str) -> Result<EventTime>,
{
    loop {
        let input: String = Input::new().with_prompt(prompt).interact_text()?;
        match parse(&input) {
            Ok(result) => return Ok(result),
            Err(e) => {
                eprintln!("  {}", e.to_string().red());
            }
        }
    }
}

/// Prompt for duration with retry on parse errors. Only accepts duration
/// strings like "30m", "1h", "2 days" — never a date or end time.
fn prompt_duration(start: &EventTime, default_hint: &str) -> Result<EventTime> {
    loop {
        let input: String = Input::new()
            .with_prompt(format!("  How long? ({})", default_hint))
            .default(String::new())
            .show_default(false)
            .interact_text()?;
        if input.is_empty() {
            return Ok(default_end(start));
        }
        match try_apply_duration(start, &input) {
            Ok(result) => return Ok(result),
            Err(_) => {
                eprintln!(
                    "  {}",
                    format!(
                        "Enter a duration like \"30m\", \"1h\", or \"2 days\" — got \"{}\"",
                        input
                    )
                    .red()
                );
            }
        }
    }
}

/// Expand common abbreviations that fuzzydate doesn't handle.
fn expand_abbreviations(input: &str) -> String {
    let abbrevs = [
        ("mon", "monday"),
        ("tue", "tuesday"),
        ("tues", "tuesday"),
        ("wed", "wednesday"),
        ("thu", "thursday"),
        ("thur", "thursday"),
        ("thurs", "thursday"),
        ("fri", "friday"),
        ("sat", "saturday"),
        ("sun", "sunday"),
        ("jan", "january"),
        ("feb", "february"),
        ("mar", "march"),
        ("apr", "april"),
        ("jun", "june"),
        ("jul", "july"),
        ("aug", "august"),
        ("sep", "september"),
        ("sept", "september"),
        ("oct", "october"),
        ("nov", "november"),
        ("dec", "december"),
    ];

    let mut result = String::new();
    let lower = input.to_lowercase();

    for (i, word) in lower.split_whitespace().enumerate() {
        if i > 0 {
            result.push(' ');
        }
        let expanded = abbrevs
            .iter()
            .find(|(abbr, _)| *abbr == word)
            .map(|(_, full)| *full)
            .unwrap_or(word);
        result.push_str(expanded);
    }

    result
}

/// Parse a date/time string into an EventTime.
/// Tries ISO 8601 formats first (e.g. "2026-06-09T10:00"), then falls back
/// to fuzzydate natural language parsing (e.g. "tomorrow 6pm").
fn parse_datetime(input: &str) -> Result<EventTime> {
    // Try ISO 8601 datetime first (YYYY-MM-DDTHH:MM)
    if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(input, "%Y-%m-%dT%H:%M") {
        let tzid = iana_time_zone::get_timezone().unwrap_or_else(|_| "UTC".to_string());
        return Ok(EventTime::DateTimeZoned { datetime: dt, tzid });
    }

    // Try ISO 8601 date-only (YYYY-MM-DD)
    if let Ok(d) = chrono::NaiveDate::parse_from_str(input, "%Y-%m-%d") {
        return Ok(EventTime::Date(d));
    }

    // Fall back to natural language parsing
    let expanded = expand_abbreviations(input);
    let dt = fuzzydate::parse(&expanded)
        .map_err(|_| anyhow::anyhow!("Could not parse date/time: \"{}\"", input))?;

    if has_time_component(input) {
        let tzid = iana_time_zone::get_timezone().unwrap_or_else(|_| "UTC".to_string());
        Ok(EventTime::DateTimeZoned { datetime: dt, tzid })
    } else {
        Ok(EventTime::Date(dt.date()))
    }
}

/// Check if the user's input string contains time-related tokens.
fn has_time_component(input: &str) -> bool {
    let lower = input.to_lowercase();

    // Check for "noon" or "midnight"
    if lower.contains("noon") || lower.contains("midnight") {
        return true;
    }

    // Check for am/pm patterns like "6pm", "6 pm", "11am"
    let bytes = lower.as_bytes();
    for (i, &b) in bytes.iter().enumerate() {
        if (b == b'a' || b == b'p') && i + 1 < bytes.len() && bytes[i + 1] == b'm' {
            // Check that there's a digit before (possibly with space)
            if i > 0 && bytes[i - 1].is_ascii_digit() {
                return true;
            }
            if i > 1 && bytes[i - 1] == b' ' && bytes[i - 2].is_ascii_digit() {
                return true;
            }
        }
    }

    // Check for HH:MM pattern (digit(s):digit(s))
    for (i, &b) in bytes.iter().enumerate() {
        if b == b':' {
            let has_digit_before = i > 0 && bytes[i - 1].is_ascii_digit();
            let has_digit_after = i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit();
            if has_digit_before && has_digit_after {
                return true;
            }
        }
    }

    // Check for "at" followed by a digit (e.g. "at 3", "at 15")
    if let Some(pos) = lower.find(" at ") {
        let after = &lower[pos + 4..];
        if after.starts_with(|c: char| c.is_ascii_digit()) {
            return true;
        }
    }
    // Also handle "at" at the start
    if let Some(after) = lower.strip_prefix("at ")
        && after.starts_with(|c: char| c.is_ascii_digit())
    {
        return true;
    }

    false
}

/// Verify that an explicit `--end` value matches the kind of `--start`
/// (both timed or both all-day). Mismatches like a timed start with a
/// date-only end are rejected by Google Calendar with a 400.
fn check_end_matches_start(start: &EventTime, end: &EventTime) -> Result<()> {
    match (start.is_date(), end.is_date()) {
        (true, false) => {
            anyhow::bail!("--start is all-day but --end is timed — both must be the same kind")
        }
        (false, true) => {
            anyhow::bail!("--start is timed but --end is all-day — both must be the same kind")
        }
        _ => Ok(()),
    }
}

/// Apply a duration string to a start time.
fn apply_duration(start: &EventTime, dur_input: &str) -> Result<EventTime> {
    try_apply_duration(start, dur_input)
        .with_context(|| format!("Could not parse duration: \"{}\"", dur_input))
}

fn try_apply_duration(start: &EventTime, dur_input: &str) -> Result<EventTime> {
    let std_dur = humantime::parse_duration(dur_input).map_err(|e| anyhow::anyhow!("{}", e))?;
    let chrono_dur = Duration::from_std(std_dur).context("Duration too large")?;

    match start {
        EventTime::Date(d) => Ok(EventTime::Date(*d + chrono_dur)),
        EventTime::DateTimeFloating(dt) => Ok(EventTime::DateTimeFloating(*dt + chrono_dur)),
        EventTime::DateTimeUtc(dt) => Ok(EventTime::DateTimeUtc(*dt + chrono_dur)),
        EventTime::DateTimeZoned { datetime, tzid } => Ok(EventTime::DateTimeZoned {
            datetime: *datetime + chrono_dur,
            tzid: tzid.clone(),
        }),
    }
}

/// Default end time: +1 hour for timed events, +1 day for all-day events.
fn default_end(start: &EventTime) -> EventTime {
    match start {
        EventTime::Date(d) => EventTime::Date(*d + Duration::days(1)),
        EventTime::DateTimeFloating(dt) => EventTime::DateTimeFloating(*dt + Duration::hours(1)),
        EventTime::DateTimeUtc(dt) => EventTime::DateTimeUtc(*dt + Duration::hours(1)),
        EventTime::DateTimeZoned { datetime, tzid } => EventTime::DateTimeZoned {
            datetime: *datetime + Duration::hours(1),
            tzid: tzid.clone(),
        },
    }
}

/// Parse a reminder string like "10m", "1h", "2 days" into a Reminder.
fn parse_reminder(input: &str) -> Result<Reminder> {
    Reminder::from_duration_str(input).map_err(|e| anyhow::anyhow!("{}", e))
}

/// Resolve which calendar to use.
fn resolve_calendar(
    slug: Option<String>,
    calendars: &[Calendar],
    interactive: bool,
) -> Result<&Calendar> {
    if let Some(slug) = slug {
        return calendars.iter().find(|c| c.slug == slug).ok_or_else(|| {
            let available: Vec<_> = calendars.iter().map(|c| c.slug.as_str()).collect();
            anyhow::anyhow!(
                "Calendar '{}' not found. Available: {}",
                slug,
                available.join(", ")
            )
        });
    }

    // If only one calendar, use it
    if calendars.len() == 1 {
        return Ok(&calendars[0]);
    }

    // Try the default calendar
    let caldir = Caldir::load()?;
    if let Some(default) = caldir.default_calendar()
        && let Some(cal) = calendars.iter().find(|c| c.slug == default.slug)
    {
        return Ok(cal);
    }

    // Multiple calendars, no default — ask if interactive
    if interactive {
        let items: Vec<&str> = calendars.iter().map(|c| c.slug.as_str()).collect();
        let selection = Select::new()
            .with_prompt("  Calendar")
            .items(&items)
            .default(0)
            .interact()?;
        Ok(&calendars[selection])
    } else {
        // Non-interactive with multiple calendars and no default
        let available: Vec<_> = calendars.iter().map(|c| c.slug.as_str()).collect();
        anyhow::bail!(
            "Multiple calendars found ({}). Use --calendar to specify one.",
            available.join(", ")
        )
    }
}

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

    // --- has_time_component ---

    #[test]
    fn time_component_am_pm() {
        assert!(has_time_component("tomorrow 6pm"));
        assert!(has_time_component("friday 11am"));
        assert!(has_time_component("sat 3 pm"));
        assert!(has_time_component("9AM"));
    }

    #[test]
    fn time_component_colon() {
        assert!(has_time_component("tomorrow 15:00"));
        assert!(has_time_component("march 20 9:30"));
    }

    #[test]
    fn time_component_keywords() {
        assert!(has_time_component("tomorrow noon"));
        assert!(has_time_component("friday midnight"));
    }

    #[test]
    fn time_component_at_digit() {
        assert!(has_time_component("tomorrow at 3"));
        assert!(has_time_component("friday at 15"));
        assert!(has_time_component("at 9"));
    }

    #[test]
    fn no_time_component() {
        assert!(!has_time_component("tomorrow"));
        assert!(!has_time_component("march 20"));
        assert!(!has_time_component("next friday"));
        assert!(!has_time_component("saturday"));
    }

    #[test]
    fn no_false_positive_am_in_words() {
        // "am" inside words like "amsterdam" shouldn't match
        assert!(!has_time_component("december"));
        assert!(!has_time_component("camp"));
    }

    // --- expand_abbreviations ---

    #[test]
    fn expand_day_abbreviations() {
        assert_eq!(expand_abbreviations("sat 3pm"), "saturday 3pm");
        assert_eq!(expand_abbreviations("fri 9am"), "friday 9am");
        assert_eq!(expand_abbreviations("mon"), "monday");
        assert_eq!(expand_abbreviations("thu noon"), "thursday noon");
        assert_eq!(expand_abbreviations("tues 10am"), "tuesday 10am");
    }

    #[test]
    fn expand_month_abbreviations() {
        assert_eq!(expand_abbreviations("jan 20"), "january 20");
        assert_eq!(expand_abbreviations("sep 5 3pm"), "september 5 3pm");
        assert_eq!(expand_abbreviations("sept 5"), "september 5");
    }

    #[test]
    fn expand_preserves_non_abbreviations() {
        assert_eq!(expand_abbreviations("tomorrow 6pm"), "tomorrow 6pm");
        assert_eq!(expand_abbreviations("next friday"), "next friday");
    }

    // --- parse_datetime ---

    #[test]
    fn parse_datetime_timed_returns_zoned() {
        let result = parse_datetime("tomorrow 3pm").unwrap();
        assert!(matches!(result, EventTime::DateTimeZoned { .. }));
    }

    #[test]
    fn parse_datetime_date_only_returns_date() {
        let result = parse_datetime("tomorrow").unwrap();
        assert!(matches!(result, EventTime::Date(_)));
    }

    #[test]
    fn parse_datetime_abbreviation_works() {
        let result = parse_datetime("sat 3pm").unwrap();
        assert!(matches!(result, EventTime::DateTimeZoned { .. }));
    }

    #[test]
    fn parse_datetime_absolute_date() {
        let result = parse_datetime("march 20").unwrap();
        assert!(matches!(result, EventTime::Date(_)));
        if let EventTime::Date(d) = result {
            assert_eq!(d.month(), 3);
            assert_eq!(d.day(), 20);
        }
    }

    #[test]
    fn parse_datetime_iso_datetime() {
        let result = parse_datetime("2026-06-09T10:00").unwrap();
        assert!(matches!(result, EventTime::DateTimeZoned { .. }));
        if let EventTime::DateTimeZoned { datetime, .. } = result {
            assert_eq!(
                datetime.date(),
                NaiveDate::from_ymd_opt(2026, 6, 9).unwrap()
            );
            assert_eq!(datetime.hour(), 10);
            assert_eq!(datetime.minute(), 0);
        }
    }

    #[test]
    fn parse_datetime_iso_date_only() {
        let result = parse_datetime("2026-06-09").unwrap();
        assert!(matches!(result, EventTime::Date(_)));
        if let EventTime::Date(d) = result {
            assert_eq!(d, NaiveDate::from_ymd_opt(2026, 6, 9).unwrap());
        }
    }

    #[test]
    fn parse_datetime_invalid_input() {
        assert!(parse_datetime("not a date at all xyz").is_err());
    }

    // --- default_end ---

    #[test]
    fn default_end_allday_adds_one_day() {
        let start = EventTime::Date(NaiveDate::from_ymd_opt(2026, 3, 20).unwrap());
        let end = default_end(&start);
        assert_eq!(
            end,
            EventTime::Date(NaiveDate::from_ymd_opt(2026, 3, 21).unwrap())
        );
    }

    #[test]
    fn default_end_timed_adds_one_hour() {
        let start = EventTime::DateTimeFloating(
            NaiveDate::from_ymd_opt(2026, 3, 20)
                .unwrap()
                .and_hms_opt(15, 0, 0)
                .unwrap(),
        );
        let end = default_end(&start);
        assert_eq!(
            end,
            EventTime::DateTimeFloating(
                NaiveDate::from_ymd_opt(2026, 3, 20)
                    .unwrap()
                    .and_hms_opt(16, 0, 0)
                    .unwrap()
            )
        );
    }

    // --- try_apply_duration ---

    #[test]
    fn apply_duration_minutes() {
        let start = EventTime::DateTimeFloating(
            NaiveDate::from_ymd_opt(2026, 3, 20)
                .unwrap()
                .and_hms_opt(15, 0, 0)
                .unwrap(),
        );
        let end = try_apply_duration(&start, "30m").unwrap();
        assert_eq!(
            end,
            EventTime::DateTimeFloating(
                NaiveDate::from_ymd_opt(2026, 3, 20)
                    .unwrap()
                    .and_hms_opt(15, 30, 0)
                    .unwrap()
            )
        );
    }

    #[test]
    fn apply_duration_hours() {
        let start = EventTime::DateTimeFloating(
            NaiveDate::from_ymd_opt(2026, 3, 20)
                .unwrap()
                .and_hms_opt(14, 0, 0)
                .unwrap(),
        );
        let end = try_apply_duration(&start, "2hours").unwrap();
        assert_eq!(
            end,
            EventTime::DateTimeFloating(
                NaiveDate::from_ymd_opt(2026, 3, 20)
                    .unwrap()
                    .and_hms_opt(16, 0, 0)
                    .unwrap()
            )
        );
    }

    #[test]
    fn apply_duration_to_allday() {
        let start = EventTime::Date(NaiveDate::from_ymd_opt(2026, 3, 20).unwrap());
        let end = try_apply_duration(&start, "3days").unwrap();
        assert_eq!(
            end,
            EventTime::Date(NaiveDate::from_ymd_opt(2026, 3, 23).unwrap())
        );
    }

    // --- check_end_matches_start ---

    #[test]
    fn check_end_matches_start_both_timed_ok() {
        let start = EventTime::DateTimeFloating(
            NaiveDate::from_ymd_opt(2026, 3, 20)
                .unwrap()
                .and_hms_opt(15, 0, 0)
                .unwrap(),
        );
        let end = EventTime::DateTimeFloating(
            NaiveDate::from_ymd_opt(2026, 3, 20)
                .unwrap()
                .and_hms_opt(16, 0, 0)
                .unwrap(),
        );
        assert!(check_end_matches_start(&start, &end).is_ok());
    }

    #[test]
    fn check_end_matches_start_both_dates_ok() {
        let start = EventTime::Date(NaiveDate::from_ymd_opt(2026, 3, 20).unwrap());
        let end = EventTime::Date(NaiveDate::from_ymd_opt(2026, 3, 21).unwrap());
        assert!(check_end_matches_start(&start, &end).is_ok());
    }

    #[test]
    fn check_end_matches_start_timed_start_date_end_errors() {
        let start = EventTime::DateTimeFloating(
            NaiveDate::from_ymd_opt(2026, 3, 20)
                .unwrap()
                .and_hms_opt(15, 0, 0)
                .unwrap(),
        );
        let end = EventTime::Date(NaiveDate::from_ymd_opt(2026, 3, 20).unwrap());
        assert!(check_end_matches_start(&start, &end).is_err());
    }

    #[test]
    fn check_end_matches_start_date_start_timed_end_errors() {
        let start = EventTime::Date(NaiveDate::from_ymd_opt(2026, 3, 20).unwrap());
        let end = EventTime::DateTimeFloating(
            NaiveDate::from_ymd_opt(2026, 3, 20)
                .unwrap()
                .and_hms_opt(15, 0, 0)
                .unwrap(),
        );
        assert!(check_end_matches_start(&start, &end).is_err());
    }
}
