use jam_types::ServiceId;
use log::{kv::Key, Log, Metadata, Record};
use std::{
	cell::Cell,
	sync::Arc,
	time::{Duration, SystemTime},
};

mod cache;
pub use self::cache::*;

/// The key that should be used to capture logs as belonging to a specific service.
///
/// Example: `log::debug!(service_id = 123; "Hello");`
pub const SERVICE_ID_KEY: &str = "service_id";

thread_local! {
	/// ID of the service to which current thread's logs belong.
	static CURRENT_SERVICE_ID: Cell<Option<ServiceId>> = const { Cell::new(None) };
}

/// This logger captures messages belonging to a specific service and stores them in an in-memory
/// cache.
///
/// All messages are forwarded to the wrapped logger whether they belong to a service or not.
pub struct ServiceLogger<L> {
	inner: L,
	logs: Arc<LogCache>,
}

impl<L> ServiceLogger<L> {
	pub fn wrap(inner: L, logs: Arc<LogCache>) -> Self {
		Self { inner, logs }
	}

	fn capture(&self, record: &Record) {
		if let Some(service_id) = record
			.key_values()
			.get(Key::from(SERVICE_ID_KEY))
			.and_then(|value| value.to_u64().map(|value| value as ServiceId))
			.or_else(|| CURRENT_SERVICE_ID.get())
		{
			let time = SystemTime::now()
				.duration_since(SystemTime::UNIX_EPOCH)
				.unwrap_or(Duration::ZERO)
				.as_secs();
			self.logs.insert(
				service_id,
				time,
				record.level(),
				record.target().to_owned(),
				record.args().to_string(),
			);
		}
	}
}

impl<L: Log> Log for ServiceLogger<L> {
	#[inline]
	fn enabled(&self, metadata: &Metadata) -> bool {
		self.inner.enabled(metadata)
	}

	fn log(&self, record: &Record) {
		if self.enabled(record.metadata()) {
			self.inner.log(record);
			self.capture(record);
		}
	}

	#[inline]
	fn flush(&self) {
		self.inner.flush();
	}
}

/// Consider all logs produced by the current thread as the logs of the specified service until the
/// guard object is dropped.
#[must_use]
#[inline]
pub(crate) fn capture_logs_as(service_id: ServiceId) -> LogCaptureGuard {
	CURRENT_SERVICE_ID.set(Some(service_id));
	LogCaptureGuard { prevent_instantiation: () }
}

pub(crate) struct LogCaptureGuard {
	#[allow(unused)]
	prevent_instantiation: (),
}

impl Drop for LogCaptureGuard {
	fn drop(&mut self) {
		CURRENT_SERVICE_ID.set(None);
	}
}
