use crate::algorithm::Algorithm;
use crate::canonicalization::Canonicalization;
use crate::config::Config;
use crate::parsed_message::{ParsedHeader, ParsedMessage};
use anyhow::{anyhow, Result};
use base64::{engine::general_purpose, Engine as _};
use sha2::{Digest, Sha256};
use sqlx::types::time::OffsetDateTime;
use sqlx::SqlitePool;
use tokio::time::{sleep, Duration};

pub struct Signature {
	algorithm: Algorithm,
	canonicalization: Canonicalization,
	selector: String,
	sdid: String,
	timestamp: i64,
	expiration: Option<u64>,
	headers: Vec<String>,
	body_hash: Vec<u8>,
	signature: Vec<u8>,
}

impl Signature {
	pub async fn new(db: &SqlitePool, cnf: &Config, msg: &ParsedMessage<'_>) -> Result<Self> {
		let algorithm = cnf.algorithm();
		let sdid = get_sdid(cnf, msg)?;
		let (selector, signing_key) = get_db_data(db, &sdid, algorithm).await?;
		let timestamp = OffsetDateTime::now_utc().unix_timestamp();
		let expiration = cnf.expiration().map(|x| x + timestamp as u64);
		let mut sig = Self {
			algorithm,
			canonicalization: cnf.canonicalization(),
			selector,
			sdid,
			timestamp,
			expiration,
			headers: get_headers(cnf, msg),
			body_hash: Vec::new(),
			signature: Vec::new(),
		};
		sig.compute_body_hash::<Sha256>(msg);
		let header_hash = sig.compute_header_hash::<Sha256>(msg);
		sig.signature = algorithm.sign(&signing_key, &header_hash)?;
		Ok(sig)
	}

	pub fn get_header(&self) -> String {
		let expiration = self
			.expiration
			.map(|x| format!(" x={x};"))
			.unwrap_or(String::new());
		format!(
			"DKIM-Signature: v=1; a={algorithm}; k={key_type}; c={canonicalization};\r\n\tt={timestamp};{expiration}\r\n\td={sdid};\r\n\ts={selector};\r\n\th={headers};\r\n\tbh={body_hash};\r\n\tb={signature}",
			algorithm=self.algorithm.display(),
			key_type=self.algorithm.key_type(),
			canonicalization=self.canonicalization.to_string(),
			selector=self.selector,
			sdid=self.sdid,
			timestamp=self.timestamp,
			expiration=expiration,
			headers=self.headers.join(":"),
			body_hash=general_purpose::STANDARD.encode(&self.body_hash),
			signature=general_purpose::STANDARD.encode(&self.signature),
		)
	}

	fn compute_body_hash<H: Digest>(&mut self, msg: &ParsedMessage<'_>) {
		let mut hasher = H::new();
		let body = self.canonicalization.process_body(msg.body);
		hasher.update(&body);
		self.body_hash = hasher.finalize().to_vec();
	}

	fn compute_header_hash<H: Digest>(&mut self, msg: &ParsedMessage<'_>) -> Vec<u8> {
		let mut hasher = H::new();
		for header_name in &self.headers {
			if let Some(raw_header) = get_header(msg, header_name) {
				let header = self.canonicalization.process_header(raw_header.raw);
				hasher.update(&header);
			}
		}
		let dkim_header = format!("{}\r\n", self.get_header());
		let mut dkim_header = self.canonicalization.process_header(dkim_header.as_bytes());
		dkim_header.pop();
		dkim_header.pop();
		hasher.update(dkim_header);
		hasher.finalize().to_vec()
	}
}

fn get_sdid(cnf: &Config, msg: &ParsedMessage<'_>) -> Result<String> {
	if let Some(header) = get_header(msg, "from") {
		if let Some(arb_pos) = header.value.iter().rposition(|&c| c == b'@') {
			let name = &header.value[arb_pos + 1..];
			let end_pos = name
				.iter()
				.position(|&c| c == b'>')
				.unwrap_or(name.len() - 2);
			if let Ok(sdid) = String::from_utf8(name[..end_pos].to_vec()) {
				if cnf.domains().contains(&sdid) {
					return Ok(sdid);
				} else {
					return Err(anyhow!(
						"unable to sign for a domain outside of the configured list: {sdid}"
					));
				}
			}
		}
	}
	Err(anyhow!("unable to determine the SDID"))
}

fn get_headers(cnf: &Config, msg: &ParsedMessage<'_>) -> Vec<String> {
	let nb_headers = cnf.headers().len() + cnf.headers_optional().len();
	let mut lst = Vec::with_capacity(nb_headers);
	for header_name in cnf.headers() {
		if let Some(name) = get_header_name(msg, header_name) {
			lst.push(name);
		} else {
			lst.push(header_name.to_string());
		}
	}
	for header_name in cnf.headers_optional() {
		if let Some(name) = get_header_name(msg, header_name) {
			lst.push(name);
		}
	}
	lst.sort();
	lst
}

fn get_header_name(msg: &ParsedMessage<'_>, header_name: &str) -> Option<String> {
	match get_header(msg, header_name) {
		Some(header) => String::from_utf8(header.name.to_vec()).ok(),
		None => None,
	}
}

fn get_header<'a>(
	msg: &'a ParsedMessage<'a>,
	header_name: &'a str,
) -> Option<&'a ParsedHeader<'a>> {
	let header_name = header_name.to_lowercase();
	msg.headers
		.iter()
		.find(|&header| header.name_lower == header_name)
}

async fn get_db_data(
	db: &SqlitePool,
	sdid: &str,
	algorithm: Algorithm,
) -> Result<(String, String)> {
	let mut ctn = 0;
	loop {
		let res: Option<(String, String)> = sqlx::query_as(crate::db::SELECT_LATEST_SIGNING_KEY)
			.bind(sdid)
			.bind(algorithm.to_string())
			.fetch_optional(db)
			.await?;
		if let Some((selector, private_key)) = res {
			return Ok((selector, private_key));
		}
		if ctn == crate::SIG_RETRY_NB_RETRY {
			return Err(anyhow!("unable to retrieve key material"));
		}
		ctn += 1;
		sleep(Duration::from_secs(crate::SIG_RETRY_SLEEP_TIME)).await;
	}
}

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

	const KEY_ED25519: &str = "Av46g0s6+qCczlLeIkSmD/yD7GX5pDjl8SVTSeVZIhc=";
	const KEY_RSA2048: &str = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDDYGEHGABdLxLbvqiuaGdYV9ndsFzdO31AaZoSfCVF0TVPeJ5B/YWyUzc6YIr7qFyfA4tiRPu3/Fy2vzYzG2lcYBkSzJsQWkiOWv6C3CpxYX2tBrAKnJx3ADyr9P3whztyKQBbSNLy+CCNJQ5967Z+PCOeiFAX4XvfCFMpwxsUo6Pv2SYi9PwOq8HwzUQHyy39zOs68XIUF85DCXiQ1kztXzu8HX8nuLX7AnLrf0ZiFGrbSbUjj/F7XVPh2QJxcBLZjrnpve73nrrYsIVnbCvfV563CuFq0PvsUofW7Ckbpwdx26AD59ssaOBNp/uK04xqPSQZyADEngcfkOBdqeHlAgMBAAECggEAYqZQd6d7z5FMQV5Uh7O5staw8Anz6dT22kY4AGtk2orXEyiBKCrdjfwMn9JNeuI42iWNBHJ2cyDeo3uK32VQ3s66+k4LYcdkaVSyu9p1J8ilD2+YBdOsAT5CZEUQz5lIv8zKHE16DiqRLRNv7M7O15CAH7UOU/CLfkMS0rxr+Q//dw94mUTXSy+XKwWgdQSxjiqcfEFEArtP4QH+BM2j7Jk+cMm4OlOklOLlcktSgrCSGp4uqt9BYTq86XFSVaZHbirKAheO7mv9xSXj46zenWaSwWDJ/zYSQHUzTwdCcm35gHeGvUvBo/njCzfvNM6xWl+vrMzD9i4pB0PZ6yi0IQKBgQD/97QWjwdCt7ubRv453lLZ2CXDNt/QFKOr+ESlDPkCxmb/DIY8HDqbJpdyPrCm6MHL8lUBLNixSUfkrCFFaGgpHyG6E5qJT0/UVVwSWUAWciOY7SOZXMPgvkahU39GzPDE8m+mihyDPT98vUqX4HTZJzannaSESGeVtADYXN416QKBgQDDZrYuD4bzjft7XNmmbdl52sfCcKgduzmooSMjeIKffSZuCvpVBA0kEgqLalLaMSCjcx95djufqDkU+RBT7lAbZDYd+lD4m4dNzGqy1hgTVfBaPOYCg4iC2WKiHZDIw8n630gFyRZDAn+KINbcSn7V2ZxqDa1ZE6wAWgiBn98CnQKBgQDSmFnytXqjycbw2lgQBHrmAJARLPS3nkOLGZhgs2usfNAAx60ph5AwVnAD7tAogxfvVFHbxaoDMueTnItDL8ODEboN/lMG5dooOJKoBgZUcVQYXgMMCuad4e76jFgLSFJPt6dkvfz3fUzetF7K1kFM6JZvEaRpsaiH4rFPUhkBAQKBgHwXepL94WJDRPYvHToIgRhVzI67JMjc4d0pmDsqiSnoPMOdzSS4ke/aVT/8oelXUbb7oX1tjKf0GWwsUCY9Ljp3Bbc8BLgdbWwG6avxMxD0ftOP4TKvfb47d9wkkpItZNQhgIfMEIs1xvFdsZXs6We97wua6/+p8o22n7hSYzoxAoGBAM9zQgkeV2U++yi4memcNX1sLnRHaBUSShj+IXhOM8Hpw5deyxFKUh2sD537CJxKOx+8XMKvWilY2MFRCZIlTAQBasEfj9+YZSLY7kFHfWKcUKhqVQ+5M+LgbuZjf6X/0Y+2Lqc585NoxmMDc4GC/1t4eagXqMvcuh10S2JP9RWp";
	const MSG_01_RAW: &[u8] = include_bytes!("../tests/test_01.msg");
	const MSG_01_HEADERS: &[&str] = &[
		"Date",
		"From",
		"Resent-Date",
		"Subject",
		"To",
		"cc",
		"reply-to",
	];

	#[test]
	fn test_simple_simple() {
		let ref_sig_header = "DKIM-Signature: v=1; a=ed25519-sha256; k=ed25519; c=simple/simple;\r\n\tt=1681595158;\r\n\td=example.org;\r\n\ts=dkim-b3fb546a27bb44dd88a1fd2b4b3e2e96;\r\n\th=Date:From:Resent-Date:Subject:To:cc:reply-to;\r\n\tbh=z85OKVJZHnmg3qFlSpLbpPCZ00irfBdrzQUtabiSl3A=;\r\n\tb=YGbtIaodjitU3jNtwRA2+AJ/i5W4VxmZuESKnv08ofZ7nuYLUiqaS1sR3DGeJ1t83R5ZmPSJAQPBYRg3usJxBg==";
		let msg = ParsedMessage::from_bytes(MSG_01_RAW).unwrap();
		let mut sig = Signature {
			algorithm: Algorithm::Ed25519Sha256,
			canonicalization: "simple/simple".parse().unwrap(),
			selector: "dkim-b3fb546a27bb44dd88a1fd2b4b3e2e96".into(),
			sdid: "example.org".into(),
			timestamp: 1681595158,
			expiration: None,
			headers: MSG_01_HEADERS.iter().map(|h| h.to_string()).collect(),
			body_hash: Vec::new(),
			signature: Vec::new(),
		};
		sig.compute_body_hash::<Sha256>(&msg);
		let header_hash = sig.compute_header_hash::<Sha256>(&msg);
		sig.signature = sig.algorithm.sign(KEY_ED25519, &header_hash).unwrap();
		assert_eq!(sig.get_header(), ref_sig_header);
	}

	#[test]
	fn test_relaxed_relaxed() {
		let ref_sig_header = "DKIM-Signature: v=1; a=rsa-sha256; k=rsa; c=relaxed/relaxed;\r\n\tt=1681593844; x=1682889844;\r\n\td=example.org;\r\n\ts=dkim-681d955d9fc84d978d71a7d7f8ce7dd6;\r\n\th=Date:From:Resent-Date:Subject:To:cc:reply-to;\r\n\tbh=z85OKVJZHnmg3qFlSpLbpPCZ00irfBdrzQUtabiSl3A=;\r\n\tb=mvdT944tidhpbJHJdbYtIedkXIxNERP7xpAXjWxen5tTULYD72JCQBQy7HbMqix3S0JAex6VVdyqmMjEC053yWNOckVH5E62sGaMyDj8Us1isTmcqIu3VSSQhLpMKkdMv55esqMoaTNC+L+I9p44AHst64sodJmbDA33vLhqoGRja8IylrSK0O4XqgWl2XzGfcyXuDT8miO1NLUU3Hgfgs7edRjBkF2iTMraWiObr0ZW2vI9+Ib9DeuupcC3GMg1MheWlmWNnHlrlSmnkd2VuWEX/ydqlxxXz3/oPjY5ATgYVPBc8apk4KkMDqoQ9EkUvItsvDUz0UhhbXMP3pGGrA==";
		let msg = ParsedMessage::from_bytes(MSG_01_RAW).unwrap();
		let mut sig = Signature {
			algorithm: Algorithm::Rsa2048Sha256,
			canonicalization: "relaxed/relaxed".parse().unwrap(),
			selector: "dkim-681d955d9fc84d978d71a7d7f8ce7dd6".into(),
			sdid: "example.org".into(),
			timestamp: 1681593844,
			expiration: Some(1682889844),
			headers: MSG_01_HEADERS.iter().map(|h| h.to_string()).collect(),
			body_hash: Vec::new(),
			signature: Vec::new(),
		};
		sig.compute_body_hash::<Sha256>(&msg);
		let header_hash = sig.compute_header_hash::<Sha256>(&msg);
		sig.signature = sig.algorithm.sign(KEY_RSA2048, &header_hash).unwrap();
		assert_eq!(sig.get_header(), ref_sig_header);
	}
}
