use std::fmt::Display;

use diqwest::WithDigestAuth;
use icalendar::{Calendar};
use minidom::Element;
use reqwest::{
	header::{CONTENT_TYPE, USER_AGENT}, Client, Method, RequestBuilder, Url
};
use serde::{Deserialize, Serialize};

static PRINCIPAL_BODY: &str = r#"
<d:propfind xmlns:d="DAV:">
	<d:prop>
		<d:current-user-principal />
	</d:prop>
</d:propfind>
"#;

static HOMESET_BODY: &str = r#"
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
	<d:prop>
		<c:calendar-home-set />
	</d:prop>
</d:propfind>
"#;

static CAL_BODY: &str = r#"
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" >
	<d:prop>
		<d:displayname />
		<d:resourcetype />
		<c:supported-calendar-component-set />
	</d:prop>
</d:propfind>
"#;

static EVENT_BODY: &str = r#"
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
	<d:prop>
		<d:getetag />
		<c:calendar-data />
	</d:prop>
	<c:filter>
		<c:comp-filter name="VCALENDAR">
			<c:comp-filter name="VEVENT" >
				<c:time-range start="{start}" end="{end}" />
			</c:comp-filter>
		</c:comp-filter>
	</c:filter>
</c:calendar-query>
"#;

pub fn find_elems(root: &Element, tag: String) -> Vec<&Element> {
	let mut elems: Vec<&Element> = Vec::new();

	for el in root.children() {
		if el.name() == tag {
			elems.push(el);
		} else {
			let ret = find_elems(el, tag.clone());
			elems.extend(ret);
		}
	}
	elems
}

pub fn find_elem(root: &Element, tag: String) -> Option<&Element> {
	if root.name() == tag {
		return Some(root);
	}

	for el in root.children() {
		if el.name() == tag {
			return Some(el);
		} else {
			let ret = find_elem(el, tag.clone());
			if ret.is_some() {
				return ret;
			}
		}
	}
	None
}

#[derive(Clone, Serialize, Deserialize)]
pub enum AuthScheme {
	Basic,
	Digest,
}

impl std::str::FromStr for AuthScheme {
	type Err = String;

	fn from_str(s: &str) -> Result<Self, Self::Err> {
		match s.to_lowercase().as_str() {
			"basic" => Ok(AuthScheme::Basic),
			"digest" => Ok(AuthScheme::Digest),
			_ => Err(format!("Invalid auth scheme: {}", s)),
		}
	}
}

impl Display for AuthScheme {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			AuthScheme::Basic => write!(f, "Basic"),
			AuthScheme::Digest => write!(f, "Digest"),
		}
	}
}

pub trait ReqwestAuth {
	fn send_authed(self, scheme: &AuthScheme, username: &str, password: &str) -> impl std::future::Future<Output = anyhow::Result<reqwest::Response>> + Send;
}

impl ReqwestAuth for RequestBuilder {
	fn send_authed(self, scheme: &AuthScheme, username: &str, password: &str) -> impl std::future::Future<Output = anyhow::Result<reqwest::Response>> + Send {
		async move {
			let res = match scheme {
				AuthScheme::Basic => self.basic_auth(username, Some(password)).send().await?,
				AuthScheme::Digest => self.send_with_digest_auth(username, password).await?,
			};
			Ok(res)
		}
	}
}

pub async fn request(
	client: &Client,
	method: Method,
	url: Url,
	body: String,
	depth: u8,
	credentials: &DAVCredentials,
) -> anyhow::Result<Element> {
	let res = client
		.request(method, url)
		.header("Depth", depth.to_string())
		.header(USER_AGENT, "TrafficCue")
		.header(CONTENT_TYPE, "application/xml")
		.body(body)
		.send_authed(&credentials.scheme, &credentials.username, &credentials.password)
		.await?;
	let status = res.status();
	if !status.is_success() {
		Err(anyhow::anyhow!("Request failed with status: {}", status))?;
	}

	let text = res.text().await?;
	let root: Element = text.parse()?;
	Ok(root)
}

#[derive(Clone, Serialize, Deserialize)]
pub struct DAVCredentials {
	pub scheme: AuthScheme,
	pub username: String,
	pub password: String,
}

impl DAVCredentials {
	pub async fn find_scheme(url: &Url) -> anyhow::Result<AuthScheme> {
		let client = Client::new();
		let res = client
			.request(Method::from_bytes(b"PROPFIND").expect("Invalid method"), url.clone())
			.header(USER_AGENT, "TrafficCue")
			.send()
			.await;
		if res.is_err() {
			return Err(anyhow::anyhow!("Failed to connect to server"));
		}
		let res = res.unwrap();
		if res.status().is_success() {
			return Err(anyhow::anyhow!("Server did not require authentication"));
		}
		let headers = res.headers();
		if let Some(www_auth) = headers.get("www-authenticate") {
			let www_auth = www_auth.to_str().unwrap_or("");
			if www_auth.to_lowercase().contains("digest") {
				return Ok(AuthScheme::Digest);
			} else if www_auth.to_lowercase().contains("basic") {
				return Ok(AuthScheme::Basic);
			}
		}
		Err(anyhow::anyhow!("Could not determine authentication scheme"))
	}
}

/// Client for interacting with a CalDAV server.
/// ## Example
/// ```
/// let client = DAVClient::new("https://cal.example.com/.well-known/caldav", DAVCredentials {
///     username: "user".into(),
///     password: "pass".into(),
/// });
/// client.init().await?;
/// let calendars = client.get_calendars().await?;
/// for cal in calendars {
///     println!("{}", cal);
///     let events = cal.get_events(&client.credentials).await?;
///     for event in events {
///         println!("Event:\n{}", event);
///     }
/// }
/// ```
pub struct DAVClient {
	pub url: Url,
	pub credentials: DAVCredentials,
	client: Client,

	principal_url: Option<Url>,
	cal_url: Option<Url>,
}

impl DAVClient {
	/// **Ensure the URL is actually the CalDAV endpoint, and not just the base URL.**
	pub fn new(string_url: &str, credentials: DAVCredentials) -> Self {
		let url = Url::parse(string_url).expect("Invalid URL");
		Self {
			url,
			credentials,
			client: Client::new(),
			principal_url: None,
			cal_url: None,
		}
	}

	pub async fn init(&mut self) -> anyhow::Result<()> {
		self.get_principal().await?;
		self.get_cal_homeset().await?;
		Ok(())
	}

	pub async fn _propfind(
		&mut self,
		url: Url,
		body: String,
		depth: u8,
	) -> anyhow::Result<Element> {
		let method = Method::from_bytes(b"PROPFIND").expect("Invalid method");
		let root = request(
			&self.client,
			method,
			url,
			body,
			depth,
			&self.credentials,
		).await?;
		Ok(root)
	}

	pub async fn get_principal(&mut self) -> anyhow::Result<()> {
		let root: Element = self
			._propfind(self.url.clone(), PRINCIPAL_BODY.to_string(), 0)
			.await?;
		let principal = find_elem(&root, "current-user-principal".to_string())
			.ok_or(anyhow::anyhow!("No principal found"))?;
		let principal_href =
			find_elem(principal, "href".to_string()).ok_or(anyhow::anyhow!("No href found"))?;
		let h_str = principal_href.text();
		let mut p_url = self.url.clone();
		p_url.set_path(&h_str);
		self.principal_url = Some(p_url);
		Ok(())
	}

	pub async fn get_cal_homeset(&mut self) -> anyhow::Result<()> {
		if self.principal_url.is_none() {
			return Err(anyhow::anyhow!("Principal URL not set"));
		}
		let root: Element = self
			._propfind(
				self.principal_url.clone().unwrap(),
				HOMESET_BODY.to_string(),
				0,
			)
			.await?;
		let homeset = find_elem(&root, "calendar-home-set".to_string())
			.ok_or(anyhow::anyhow!("No calendar-home-set found"))?;
		let href =
			find_elem(homeset, "href".to_string()).ok_or(anyhow::anyhow!("No href found"))?;
		let h_str = href.text();
		let mut cal_url = self.url.clone();
		cal_url.set_path(&h_str);
		self.cal_url = Some(cal_url.clone());
		Ok(())
	}

	pub async fn get_calendars(&mut self) -> anyhow::Result<Vec<DAVCalendar>> {
		if self.cal_url.is_none() {
			return Err(anyhow::anyhow!("Calendar URL not set"));
		}
		let root: Element = self
			._propfind(self.cal_url.clone().unwrap(), CAL_BODY.to_string(), 1)
			.await?;
		let reps = find_elems(&root, "response".to_string());
		let mut calendars = Vec::new();
		for rep in reps {
			let displayname = find_elem(rep, "displayname".to_string())
				.ok_or(anyhow::anyhow!("No displayname found"))?
				.text();
			if displayname == "" {
				continue;
			}

			let resourcetype = find_elem(rep, "resourcetype".to_string())
				.ok_or(anyhow::anyhow!("No resourcetype found"))?;
			let is_calendar = find_elem(resourcetype, "calendar".to_string()).is_some();
			if !is_calendar {
				continue;
			}

			let url_base = self.cal_url.as_ref().unwrap();
			let mut href = url_base.clone();
			href.set_path(
				&find_elem(rep, "href".to_string())
					.ok_or(anyhow::anyhow!("No href found"))?
					.text(),
			);
			calendars.push(DAVCalendar {
				name: displayname,
				url: href,
			});
		}

		Ok(calendars)
	}
}

/// Represents a CalDAV calendar.
/// Can be used to fetch events.
#[derive(Serialize, Deserialize, Clone)]
pub struct DAVCalendar {
	pub name: String,
	pub url: Url,
}

impl Display for DAVCalendar {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		write!(f, "{} ({})", self.name, self.url)
	}
}

impl DAVCalendar {
	pub fn new(name: &str, url: Url) -> Self {
		Self {
			name: name.to_string(),
			url,
		}
	}

	pub async fn _report(
		&mut self,
		credentials: &DAVCredentials,
		body: String,
	) -> anyhow::Result<Element> {
		let client = Client::new();
		let method = Method::from_bytes(b"REPORT").expect("Invalid method");
		let root = request(
			&client,
			method,
			self.url.clone(),
			body,
			1,
			credentials,
		).await?;
		Ok(root)
	}

	pub async fn get_events(
		&mut self,
		credentials: &DAVCredentials,
	) -> anyhow::Result<Vec<Calendar>> {
		let mut events = Vec::new();
		let time = chrono::Utc::now();
		let start = time.format("%Y%m%dT000000Z").to_string();
		let end = time.format("%Y%m%dT235959Z").to_string();
		let body = EVENT_BODY.replace("{start}", &start).replace("{end}", &end);
		let root = self._report(credentials, body).await?;
		let datas = find_elems(&root, "calendar-data".to_string());
		for data in datas {
			let etext = data.text();
			let cal: Calendar = etext.parse().unwrap();
			events.push(cal);
		}

		Ok(events)
	}
}
