use time::{
	error::{Parse, TryFromParsed},
	format_description::{well_known, 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_24HOUR: &[FormatItem<'_>] = format_description!("[hour padding:zero]:[minute]");
const TIME_12HOUR: &[FormatItem<'_>] =
	format_description!("[hour padding:none]:[minute][period case:lower case_sensitive:false]");

#[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);

// Ripped from infica with the month of Sol removed. I just want the 3 letter months
/// Capitalized months (long, short) and lowercase months (long, short).
// it seems useful to have the lowercase here so we don't have to always call
// to_lowercase
const MONTHS: [[&str; 4]; 12] = [
	["January", "Jan", "january", "jan"],
	["February", "Feb", "february", "feb"],
	["March", "Mar", "march", "mar"],
	["April", "Apr", "april", "apr"],
	["May", "May", "may", "may"],
	["June", "Jun", "june", "jun"],
	["July", "Jul", "july", "jul"],
	["August", "Aug", "august", "aug"],
	["September", "Sep", "september", "sep"],
	["October", "Oct", "october", "oct"],
	["November", "Nov", "november", "nov"],
	["December", "Dec", "december", "dec"],
];

/// 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> {
	match Time::parse(raw, TIME_24HOUR) {
		Err(_e) => Time::parse(raw, TIME_12HOUR),
		Ok(t) => Ok(t),
	}
}

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 calculated_offset = us_dst_central_offset(PrimitiveDateTime::new(date, time));
	let offset = match splits.next() {
		None => calculated_offset,
		Some(raw) => {
			let offset = parse_offset(raw);
			if offset != calculated_offset {
				//FIXME: gen 2024-12; warn here but format it rightly.
				()
			}

			offset
		}
	};

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

pub fn format_long(datetime: &OffsetDateTime) -> String {
	let year = datetime.year();
	let month = MONTHS[datetime.month() as usize - 1][1];
	let day = datetime.day();
	let weekday = datetime.weekday();

	format!("{weekday} {day}, {month} {year}")
}

pub fn iso8601(datetime: &OffsetDateTime) -> String {
	datetime
		.format(&well_known::Iso8601::DATE_TIME_OFFSET)
		.unwrap()
}

#[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));
	}
}