use std::{
	array,
	fs::File,
	io::{BufRead, BufReader},
	sync::{atomic::Ordering, mpsc::Sender},
	thread::JoinHandle,
	time::Duration,
};

use regex_lite::Regex;
use time::OffsetDateTime;

use crate::{db::DbMeminfo, griph, AwakeState};

pub struct Gatherer {
	state: AwakeState,
	hwnd: Option<JoinHandle<()>>,
}

impl Gatherer {
	pub fn new(state: AwakeState) -> Self {
		Self { state, hwnd: None }
	}

	pub fn start(&mut self) {
		let state = self.state.clone();
		let hwnd = std::thread::spawn(|| task(state));
		self.hwnd = Some(hwnd);
	}
}

fn task(state: AwakeState) {
	tracing::info!("starting gatherer thread");

	if !state.do_statistics {
		tracing::warn!("statistics disabled");
		return;
	}

	// I just want a graph on first boot; don't care about divisions just yet
	make_mem_graph(&state);
	make_net_graph(&state);

	// If we collected a point less than a minute ago, like after
	// just being restarted, wait until it's been a minute
	let last_meminfo = state.database.get_last_host_meminfo();
	let since = OffsetDateTime::now_utc() - last_meminfo.stamp;
	if since < time::Duration::minutes(1) {
		let to_minute = time::Duration::minutes(1) - since;

		tracing::info!(
			"waiting for {}s to space a minute apart",
			to_minute.whole_seconds()
		);
		std::thread::sleep(to_minute.try_into().unwrap());
	}

	let mut last_netinfo: Option<Netinfo> = None;

	// this is a `let _` because otherwise the attribute was
	// making the comiler mad
	#[rustfmt::skip]
	let _ = loop {
		tracing::debug!("collecting stats");

		// Gather data
		let meminfo = Meminfo::current();
		let netinfo = Netinfo::current();

		// Print traces, y'know, for tracing
		tracing::trace!("memory: {}MB used / {}MB total", meminfo.usage() / 1000, meminfo.total / 1000);
		tracing::trace!("net: rx {} / tx {}", data_human_fmt(netinfo.rx_bytes), data_human_fmt(netinfo.tx_bytes));

		if let Some(lni) = last_netinfo {
			let rx_delta = netinfo.rx_bytes - lni.rx_bytes;
			let tx_delta = netinfo.tx_bytes - lni.tx_bytes;

			state.database.insert_hostnet(60, rx_delta, tx_delta);
		}
		last_netinfo = Some(netinfo);

		// Store stats in database
		state.database.insert_host_meminfo(meminfo);

		// Only generate graphs every 15 minutes
		let now = OffsetDateTime::now_utc();
		if now.minute() % 15 == 0 {
			make_mem_graph(&state);
			make_net_graph(&state);
		}

		std::thread::sleep(Duration::from_secs(60));
	};
}

pub fn make_mem_graph(state: &AwakeState) {
	tracing::debug!("generating meminfo graph");

	let infos = state.database.get_last_n_host_meminfo(256);
	let max = infos[0].total_kb;

	let now = OffsetDateTime::now_utc();
	let cleaned = clean_series(&infos, |mem| mem.stamp, now);
	let mut usages: Vec<Option<usize>> = cleaned
		.into_iter()
		.map(|mi| mi.map(|mi| mi.usage()))
		.collect();

	// Reversing here because we want latest valeus on on the
	// right side, so last in the array
	usages.reverse();

	let gif = griph::make_1line(0, max, &usages);

	let path = state.cache_path.join("current_hostmeminfo.gif");
	gif.save(path).unwrap();
}

pub fn make_net_graph(state: &AwakeState) {
	tracing::debug!("generating netinfo graph");

	let now = OffsetDateTime::now_utc();
	let infos = state.database.get_last_n_hostnet(256);
	let cleaned = clean_series(&infos, |net| net.stamp, now);

	// 125 is (1000 / 8) so it converst Bytes to kiloBITS
	let mut rx_deltas = extract(&cleaned, |ni| ni.rx_bytes_per_sec() as usize / 125);
	let mut tx_deltas = extract(&cleaned, |ni| ni.tx_bytes_per_sec() as usize / 125);

	// Reversing to put latest values on the right side
	rx_deltas.reverse();
	tx_deltas.reverse();

	let rx_zeroed: Vec<usize> = cleaned
		.iter()
		.map(|m| m.map(|n| n.rx_bytes_per_sec() as usize / 125).unwrap_or(0))
		.collect();

	let tx_zeroed: Vec<usize> = cleaned
		.iter()
		.map(|m| m.map(|n| n.tx_bytes_per_sec() as usize / 125).unwrap_or(0))
		.collect();

	// Mixing the TX/RX delta so we can pick a range.
	let mut mixed = vec![0; 512];
	mixed[..256].copy_from_slice(&rx_zeroed);
	mixed[256..].copy_from_slice(&tx_zeroed);

	mixed.sort();
	let kinda_highest = mixed[511 - 32];
	let high_bound = (kinda_highest as f32 / 256.0).ceil().min(1.0) as usize * 256;
	state
		.netinfo_upper_bound
		.store(high_bound, Ordering::Release);

	let gif = griph::make_2line(0, high_bound, &tx_deltas, &rx_deltas);

	let path = state.cache_path.join("current_hostnetinfo.gif");
	gif.save(path).unwrap();
}

fn clean_series<T, F>(series: &[T], time_extractor: F, end_time: OffsetDateTime) -> [Option<T>; 256]
where
	F: Fn(&T) -> OffsetDateTime,
	T: Clone,
{
	let mut res = [const { None }; 256];

	for value in series {
		let time = time_extractor(value);
		let delta = end_time - time;
		let mins = delta.whole_minutes();

		if mins > 0 && mins < 256 {
			res[mins as usize] = Some(value.clone());
		}
	}

	res
}

fn extract<T, F, V>(series: &[Option<T>], extractor: F) -> [Option<V>; 256]
where
	F: Fn(&T) -> V,
{
	let mut res = [const { None }; 256];
	for (idx, maybe) in series.iter().enumerate() {
		if let Some(value) = maybe {
			res[idx] = Some(extractor(value));
		}
	}

	res
}

pub struct Meminfo {
	pub total: usize,
	pub free: usize,
	pub avaialable: usize,
}

impl Meminfo {
	pub fn current() -> Self {
		let procinfo = File::open("/proc/meminfo").unwrap();
		let bread = BufReader::new(procinfo);

		let mut meminfo = Meminfo {
			total: 0,
			free: 0,
			avaialable: 0,
		};

		for line in bread.lines() {
			let line = line.unwrap();

			if let Some((raw_key, raw_value_kb)) = line.split_once(':') {
				let value = if let Some(raw_value) = raw_value_kb.trim().strip_suffix(" kB") {
					if let Ok(parsed) = raw_value.parse() {
						parsed
					} else {
						continue;
					}
				} else {
					continue;
				};

				match raw_key.trim() {
					"MemTotal" => meminfo.total = value,
					"MemFree" => meminfo.free = value,
					"MemAvailable" => meminfo.avaialable = value,
					_ => (),
				}
			}
		}

		meminfo
	}

	pub fn usage(&self) -> usize {
		self.total - self.avaialable
	}
}

pub struct Netinfo {
	rx_bytes: usize,
	tx_bytes: usize,
}

impl Netinfo {
	pub fn current() -> Self {
		let procinfo = File::open("/proc/net/dev").unwrap();
		let bread = BufReader::new(procinfo);

		let mut netinfo = Self {
			rx_bytes: 0,
			tx_bytes: 0,
		};

		let re = Regex::new(r"[ ]*(\d+)").unwrap();
		let interface = "eth0:";
		for line in bread.lines() {
			let line = line.unwrap();
			let trim = line.trim();

			let mut captures = if let Some(data) = trim.strip_prefix(interface) {
				re.captures_iter(data)
			} else {
				continue;
			};

			netinfo.rx_bytes = captures
				.next()
				.unwrap()
				.get(1)
				.unwrap()
				.as_str()
				.parse()
				.unwrap();
			netinfo.tx_bytes = captures
				.skip(7)
				.next()
				.unwrap()
				.get(1)
				.unwrap()
				.as_str()
				.parse()
				.unwrap();
			break;
		}

		netinfo
	}
}

fn data_human_fmt(bytes: usize) -> String {
	let (num, unit) = data_human(bytes);
	format!("{num}{unit}")
}

fn data_human(bytes: usize) -> (f32, &'static str) {
	const UNITS: &[&str] = &["B", "kB", "MB", "GB", "TB"];

	let mut wrk = bytes as f32;
	let mut unit_idx = 0;
	loop {
		if wrk < 1500.0 || unit_idx == UNITS.len() - 1 {
			return (wrk, UNITS[unit_idx]);
		}
		wrk /= 1000.0;
		unit_idx += 1;
	}
}