use time::{
	error::{Parse, TryFromParsed},
	format_description::FormatItem,
	macros::{format_description, offset, time},
	Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday,
};

const FMT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day] [hour]:[minute]");

const DATE: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]");
const TIME: &[FormatItem<'_>] = format_description!("[hour]:[minute]");

#[allow(unused)]
const OFFSET: &[FormatItem<'_>] =
	format_description!("[offset_hour sign:mandatory]:[offset_minute]");

const CST: UtcOffset = offset!(-06:00);
const CDT: UtcOffset = offset!(-05:00);

/// Offset for the united states in Central Time. Accounts for DST
/// DST starts the 2nd sunday in March and ends the 1st sunday in November.
/// https://www.nist.gov/pml/time-and-frequency-division/popular-links/daylight-saving-time-dst
fn us_dst_central_offset(datetime: PrimitiveDateTime) -> UtcOffset {
	let second_sunday_march = {
		let mut seen_sunday = false;
		let mut curr = Date::from_calendar_date(datetime.year(), time::Month::March, 1).unwrap();

		loop {
			if curr.weekday() == Weekday::Sunday {
				if seen_sunday {
					break PrimitiveDateTime::new(curr, time!(02:00 AM));
				} else {
					seen_sunday = true;
				}
			}

			curr = curr.next_day().unwrap();
		}
	};

	let first_sunday_november = {
		let mut curr = Date::from_calendar_date(datetime.year(), time::Month::November, 1).unwrap();

		loop {
			if curr.weekday() == Weekday::Sunday {
				break PrimitiveDateTime::new(curr, time!(02:00 AM));
			} else {
				curr = curr.next_day().unwrap();
			}
		}
	};

	if datetime >= second_sunday_march && datetime < first_sunday_november {
		CDT
	} else {
		CST
	}
}

fn parse_offset(raw: &str) -> UtcOffset {
	match raw.to_ascii_uppercase().as_str() {
		"CST" => CST,
		"CDT" => CDT,
		_ => unimplemented!(),
	}
}

fn parse_time(raw: &str) -> Result<Time, time::error::Parse> {
	Time::parse(raw, TIME)
}

fn parse_date(raw: &str) -> Result<Date, time::error::Parse> {
	Date::parse(raw, DATE)
}

/// Parses an OffsetDateTime from any of the following formats:
/// - date only: 2024-04-13
/// - date and time: 2024-04-13 2:56
/// - dat, time, and offset: 2024-04-13 2:56 CDT
///
/// If a time is not procided, noon (12:00) is assumed.
///
/// If an offset is not provided it will be assumed to be US Central Time and
/// it will correct for daylight savings.
pub fn parse(raw: &str) -> Result<OffsetDateTime, time::error::Parse> {
	let mut splits = raw.trim().split(' ');

	let date = match splits.next() {
		None => return Err(Parse::TryFromParsed(TryFromParsed::InsufficientInformation)),
		Some(raw) => parse_date(raw)?,
	};

	let time = match splits.next() {
		None => time!(12:00:00),
		Some(raw) => parse_time(raw)?,
	};

	let offset = match splits.next() {
		None => us_dst_central_offset(PrimitiveDateTime::new(date, time)),
		Some(raw) => parse_offset(raw),
	};

	Ok(OffsetDateTime::new_in_offset(date, time, offset))
}

#[cfg(test)]
mod test {
	use time::{
		macros::{datetime, offset},
		OffsetDateTime, PrimitiveDateTime,
	};

	use crate::timeparse::{parse_offset, us_dst_central_offset, CDT, CST, FMT};

	#[test]
	fn calculates_cst_cdt_correctly() {
		fn daylight(raw: &str) {
			let date = PrimitiveDateTime::parse(raw, FMT).unwrap();

			if us_dst_central_offset(date) != CDT {
				panic!("CDT failed on {raw}")
			}
		}

		fn standard(raw: &str) {
			let date = PrimitiveDateTime::parse(raw, FMT).unwrap();

			if us_dst_central_offset(date) != CST {
				panic!("CST failed on {raw}")
			}
		}

		// make sure CST and CDT are what we expect
		assert_eq!(CST, offset!(-06:00));
		assert_eq!(CDT, offset!(-05:00));

		daylight("2024-04-13 02:56");
		daylight("2024-03-10 14:00");
		daylight("2024-03-10 02:00");

		standard("2024-12-01 00:00");
		standard("2024-11-03 14:00");
		standard("2024-11-03 02:00");

		// other years
		daylight("2023-03-12 12:00"); // DST start Mar 12, end Nov 5
		standard("2023-03-11 23:00");

		standard("2023-11-05 12:00");
		daylight("2023-11-04 23:00");
	}

	#[test]
	fn paress_timezone_code() {
		assert_eq!(parse_offset("CST"), CST);
		assert_eq!(parse_offset("CDT"), CDT);
	}

	fn test_parse(raw: &str, expected: OffsetDateTime) {
		assert_eq!(super::parse(raw).unwrap(), expected)
	}

	#[test]
	fn parse_date_only() {
		test_parse("2024-04-13", datetime!(2024-04-13 12:00 -05:00));
		test_parse("2024-01-01", datetime!(2024-01-01 12:00 -06:00));
	}
}