use std::sync::LazyLock;

use chrono::{Datelike, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Weekday};
use regex::Regex;

use crate::model::{DateOrder, WeekStart};

static ISO_DATE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"\b(\d{4})-(\d{2})-(\d{2})\b").expect("valid ISO date regex"));
static NUMERIC_DATE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"\b(\d{1,4})[./-](\d{1,2})[./-](\d{1,4})\b").expect("valid numeric date regex")
});
static IN_DAYS: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"\bin\s+(\d+)\s+days?\b").expect("valid relative date regex"));
static TIME_12H: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"\b(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b").expect("valid 12-hour time regex")
});
static TIME_24H: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"\b(?:at\s+)([01]?\d|2[0-3]):([0-5]\d)\b").expect("valid 24-hour time regex")
});

#[derive(Debug, Clone, Copy)]
pub struct DateParseConfig<'a> {
    pub date_order: DateOrder,
    pub week_start: WeekStart,
    pub default_time: &'a str,
    pub morning_time: &'a str,
    pub afternoon_time: &'a str,
    pub evening_time: &'a str,
}

impl Default for DateParseConfig<'static> {
    fn default() -> Self {
        Self {
            date_order: DateOrder::YearMonthDay,
            week_start: WeekStart::Monday,
            default_time: "09:00",
            morning_time: "09:00",
            afternoon_time: "13:00",
            evening_time: "18:00",
        }
    }
}

pub fn extract_when_configured(text: &str, config: DateParseConfig<'_>) -> Option<String> {
    extract_when_from(text, Local::now().naive_local(), config)
}

fn extract_when_from(
    text: &str,
    now: NaiveDateTime,
    config: DateParseConfig<'_>,
) -> Option<String> {
    let lower = text.to_lowercase();
    let mut date = None;

    if let Some(captures) = ISO_DATE.captures(&lower) {
        date = NaiveDate::from_ymd_opt(
            captures[1].parse().ok()?,
            captures[2].parse().ok()?,
            captures[3].parse().ok()?,
        );
    }

    if date.is_none()
        && let Some(captures) = NUMERIC_DATE.captures(&lower)
    {
        let values = [
            captures[1].parse::<i32>().ok()?,
            captures[2].parse::<i32>().ok()?,
            captures[3].parse::<i32>().ok()?,
        ];
        let (year, month, day) = match config.date_order {
            DateOrder::MonthDayYear => (values[2], values[0], values[1]),
            DateOrder::DayMonthYear => (values[2], values[1], values[0]),
            DateOrder::YearMonthDay => (values[0], values[1], values[2]),
        };
        let year = if year < 100 { year + 2000 } else { year };
        date = NaiveDate::from_ymd_opt(year, month as u32, day as u32);
    }

    if date.is_none() {
        if let Some(captures) = IN_DAYS.captures(&lower) {
            date = Some(now.date() + Duration::days(captures[1].parse().ok()?));
        } else if lower.contains("day after tomorrow") {
            date = Some(now.date() + Duration::days(2));
        } else if lower.contains("tomorrow") {
            date = Some(now.date() + Duration::days(1));
        } else if lower.contains("today") || lower.contains("tonight") {
            date = Some(now.date());
        }
    }

    if date.is_none() && (lower.contains("this week") || lower.contains("next week")) {
        let day_from_start = if config.week_start == WeekStart::Sunday {
            now.weekday().num_days_from_sunday() as i64
        } else {
            now.weekday().num_days_from_monday() as i64
        };
        let current_start = now.date() - Duration::days(day_from_start);
        date = Some(if lower.contains("next week") {
            current_start + Duration::weeks(1)
        } else {
            current_start
        });
    }

    if date.is_none() {
        let weekdays = [
            ("monday", Weekday::Mon),
            ("tuesday", Weekday::Tue),
            ("wednesday", Weekday::Wed),
            ("thursday", Weekday::Thu),
            ("friday", Weekday::Fri),
            ("saturday", Weekday::Sat),
            ("sunday", Weekday::Sun),
        ];
        for (word, weekday) in weekdays {
            if lower.contains(word) {
                let mut delta = (weekday.num_days_from_monday() as i64
                    - now.weekday().num_days_from_monday() as i64
                    + 7)
                    % 7;
                if delta == 0 {
                    delta += 7;
                }
                date = Some(now.date() + Duration::days(delta));
                break;
            }
        }
    }

    let date = date?;
    let time = if let Some(captures) = TIME_12H.captures(&lower) {
        let mut hour: u32 = captures[1].parse().ok()?;
        let minute: u32 = captures
            .get(2)
            .map_or("0", |value| value.as_str())
            .parse()
            .ok()?;
        if hour == 12 {
            hour = 0;
        }
        if &captures[3] == "pm" {
            hour += 12;
        }
        NaiveTime::from_hms_opt(hour, minute, 0)?
    } else if let Some(captures) = TIME_24H.captures(&lower) {
        NaiveTime::from_hms_opt(captures[1].parse().ok()?, captures[2].parse().ok()?, 0)?
    } else if lower.contains("morning") {
        parse_clock(config.morning_time)?
    } else if lower.contains("afternoon") {
        parse_clock(config.afternoon_time)?
    } else if lower.contains("evening") || lower.contains("tonight") {
        parse_clock(config.evening_time)?
    } else {
        parse_clock(config.default_time)?
    };

    Local
        .from_local_datetime(&date.and_time(time))
        .earliest()
        .map(|date_time| date_time.to_rfc3339())
}

fn parse_clock(value: &str) -> Option<NaiveTime> {
    NaiveTime::parse_from_str(value, "%H:%M").ok()
}

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

    fn base() -> NaiveDateTime {
        NaiveDate::from_ymd_opt(2026, 8, 16)
            .unwrap()
            .and_hms_opt(12, 0, 0)
            .unwrap()
    }

    #[test]
    fn parses_relative_dates_and_weekdays() {
        let tomorrow = extract_when_from(
            "Call Ada tomorrow at 3:30pm",
            base(),
            DateParseConfig::default(),
        )
        .unwrap();
        assert!(tomorrow.contains("2026-08-17T15:30:00"));

        let friday =
            extract_when_from("Review next Friday", base(), DateParseConfig::default()).unwrap();
        assert!(friday.contains("2026-08-21T09:00:00"));
    }

    #[test]
    fn ignores_undated_text() {
        assert_eq!(
            extract_when_from("An evergreen idea", base(), DateParseConfig::default()),
            None
        );
    }

    #[test]
    fn honors_document_date_and_named_time_settings() {
        let value = extract_when_from(
            "Planning on 18/08/2026 in the evening",
            base(),
            DateParseConfig {
                date_order: DateOrder::DayMonthYear,
                week_start: WeekStart::Monday,
                default_time: "08:30",
                morning_time: "08:00",
                afternoon_time: "14:00",
                evening_time: "19:15",
            },
        )
        .unwrap();
        assert!(value.contains("2026-08-18T19:15:00"));

        let next_week = extract_when_from(
            "Review next week",
            base(),
            DateParseConfig {
                week_start: WeekStart::Sunday,
                ..DateParseConfig::default()
            },
        )
        .unwrap();
        assert!(next_week.contains("2026-08-23T09:00:00"));
    }
}
