use anyhow::Result;
use clap::{Parser, ValueEnum};
use serde::Deserialize;
use std::{fmt::Display, str::FromStr};
use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};

//
// MARK: loglevel
//

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, ValueEnum)]
pub enum LogLevel {
	Trace,
	Debug,
	Info,
	Warn,
	Error,
}

impl Default for LogLevel {
	fn default() -> Self {
		Self::Info
	}
}

impl Display for LogLevel {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			Self::Trace => write!(f, "trace"),
			Self::Debug => write!(f, "debug"),
			Self::Info => write!(f, "info"),
			Self::Warn => write!(f, "warn"),
			Self::Error => write!(f, "error"),
		}
	}
}

//
// MARK: logconfig
//

/// Configures log levels for known sources
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
pub struct LoggingConfig {
	pub other: LogLevel,
	pub silence: LogLevel,

	// Libs
	pub libservice: LogLevel,
	pub toolbox: LogLevel,

	// Bins
	pub service: LogLevel,
	pub webpage: LogLevel,
}

impl From<LoggingConfig> for EnvFilter {
	fn from(conf: LoggingConfig) -> Self {
		// Should never fail
		#[expect(clippy::unwrap_used)]
		EnvFilter::from_str(
			&[
				//
				// Silence
				//
				// http
				format!("hyper_util={}", conf.silence),
				format!("h2={}", conf.silence),
				format!("rustls={}", conf.silence),
				format!("tower={}", conf.silence),
				//
				// Libs
				//
				format!("toolbox={}", conf.toolbox),
				format!("libservice={}", conf.libservice),
				//
				// Bins
				//
				format!("service_webpage={}", conf.service),
				format!("webpage={}", conf.webpage),
				conf.other.to_string(),
			]
			.join(","),
		)
		.unwrap()
	}
}

//
// MARK: LogCliVQ
//

/// Provides global -v and -q cli arguments.
/// Use with `#[arg(flatten)]`.
#[derive(Parser, Debug, Clone)]
pub struct LogCliVQ {
	/// Increase verbosity (can be repeated)
	#[arg(default_value = "0")]
	#[arg(short, action = clap::ArgAction::Count,global = true)]
	v: u8,

	/// Decrease verbosity (can be repeated)
	#[arg(default_value = "0")]
	#[arg(short, action = clap::ArgAction::Count, global = true)]
	q: u8,
}

impl LogCliVQ {
	pub fn into_preset(&self) -> LogFilterPreset {
		let level_i: i16 = self.v as i16 - self.q as i16;

		let preset;
		if level_i <= -2 {
			preset = LogFilterPreset::Error
		} else if level_i == -1 {
			preset = LogFilterPreset::Warn
		} else if level_i == 0 {
			preset = LogFilterPreset::Info
		} else if level_i == 1 {
			preset = LogFilterPreset::Debug
		} else if level_i >= 2 {
			preset = LogFilterPreset::Trace
		} else {
			unreachable!()
		}

		return preset;
	}
}

//
// MARK: logpreset
//

/// Provides preset configurations of [LoggingConfig]
#[derive(Debug, Deserialize, Clone, Copy)]
pub enum LogFilterPreset {
	/// Standard "error" log level
	Error,

	/// Standard "warn" log level
	Warn,

	/// Standard "info" log level.
	/// This is the default.
	Info,

	/// Standard "debug" log level
	Debug,

	/// Standard "trace" log level
	Trace,

	/// Filter for loki subscriber.
	///
	/// This is similar to `Trace`,
	/// but excludes particulary spammy sources.
	Loki,
}

impl Default for LogFilterPreset {
	fn default() -> Self {
		return Self::Info;
	}
}

impl From<LogFilterPreset> for LoggingConfig {
	fn from(val: LogFilterPreset) -> Self {
		val.get_config()
	}
}

impl LogFilterPreset {
	pub fn get_config(&self) -> LoggingConfig {
		match self {
			Self::Error => LoggingConfig {
				other: LogLevel::Error,
				silence: LogLevel::Error,

				// Libs
				libservice: LogLevel::Error,
				toolbox: LogLevel::Error,

				// Bins
				webpage: LogLevel::Error,
				service: LogLevel::Error,
			},

			Self::Warn => LoggingConfig {
				other: LogLevel::Warn,
				silence: LogLevel::Warn,

				// Libs
				libservice: LogLevel::Warn,
				toolbox: LogLevel::Warn,

				// Bins
				webpage: LogLevel::Warn,
				service: LogLevel::Warn,
			},

			Self::Info => LoggingConfig {
				other: LogLevel::Warn,
				silence: LogLevel::Warn,

				// Libs
				libservice: LogLevel::Info,
				toolbox: LogLevel::Info,

				// Bins
				webpage: LogLevel::Info,
				service: LogLevel::Info,
			},

			Self::Debug => LoggingConfig {
				other: LogLevel::Warn,
				silence: LogLevel::Warn,

				// Libs
				libservice: LogLevel::Debug,
				toolbox: LogLevel::Debug,

				// Bins
				webpage: LogLevel::Debug,
				service: LogLevel::Debug,
			},

			Self::Trace => LoggingConfig {
				other: LogLevel::Trace,
				silence: LogLevel::Warn,

				// Libs
				libservice: LogLevel::Trace,
				toolbox: LogLevel::Trace,

				// Bins
				webpage: LogLevel::Trace,
				service: LogLevel::Trace,
			},

			Self::Loki => LoggingConfig {
				other: LogLevel::Trace,
				silence: LogLevel::Warn,

				// Libs
				libservice: LogLevel::Trace,
				toolbox: LogLevel::Trace,

				// Bins
				webpage: LogLevel::Trace,
				service: LogLevel::Trace,
			},
		}
	}
}

//
// MARK: initializer
//

#[cfg(feature = "loki")]
#[derive(Deserialize, Clone)]
pub struct LokiConfig {
	pub loki_host: url::Url,
	pub loki_user: String,
	pub loki_pass: String,
	pub loki_node_name: String,
}

/// Where to print logs
pub enum LoggingTarget {
	/// Send logs to stdout
	Stdout { format: LoggingFormat },

	/// Send logs to stderr
	Stderr { format: LoggingFormat },
	/*
	/// Send logs to an IndicatifWriter.
	///
	/// This is the same as Stderr { format: Ansi {color:true} },
	/// but uses an indicatifwriter with the given multiprogress.
	Indicatif(MultiProgress),
	*/
}

/// How to print logs
#[derive(Debug, Clone, Copy, Deserialize)]
pub enum LoggingFormat {
	Ansi,
	AnsiNoColor,
	Json,
}

impl Default for LoggingFormat {
	fn default() -> Self {
		Self::Ansi
	}
}

pub struct LoggingInitializer {
	pub app_name: &'static str,

	/// If `Some`, send logs to the given loki server
	#[cfg(feature = "loki")]
	pub loki: Option<LokiConfig>,

	/// Log filter for printed logs
	pub preset: LogFilterPreset,

	/// Where to print logs
	pub target: LoggingTarget,
}

impl LoggingInitializer {
	pub fn initialize(self) -> Result<()> {
		let mut stderr_ansi_layer = None;
		let mut stderr_json_layer = None;
		let mut stdout_ansi_layer = None;
		let mut stdout_json_layer = None;
		match self.target {
			LoggingTarget::Stderr {
				format: LoggingFormat::Ansi,
			} => {
				stderr_ansi_layer = Some(
					tracing_subscriber::fmt::Layer::default()
						.without_time()
						.with_ansi(true)
						.with_writer(std::io::stderr)
						.with_filter::<EnvFilter>(self.preset.get_config().into()),
				)
			}

			LoggingTarget::Stderr {
				format: LoggingFormat::AnsiNoColor,
			} => {
				stderr_ansi_layer = Some(
					tracing_subscriber::fmt::Layer::default()
						.without_time()
						.with_ansi(false)
						.with_writer(std::io::stderr)
						.with_filter::<EnvFilter>(self.preset.get_config().into()),
				)
			}

			LoggingTarget::Stderr {
				format: LoggingFormat::Json,
			} => {
				stderr_json_layer = Some(
					tracing_subscriber::fmt::Layer::default()
						.without_time()
						.with_ansi(false)
						.json()
						.with_writer(std::io::stderr)
						.with_filter::<EnvFilter>(self.preset.get_config().into()),
				)
			}

			LoggingTarget::Stdout {
				format: LoggingFormat::Ansi,
			} => {
				stdout_ansi_layer = Some(
					tracing_subscriber::fmt::Layer::default()
						.without_time()
						.with_ansi(true)
						.with_writer(std::io::stdout)
						.with_filter::<EnvFilter>(self.preset.get_config().into()),
				)
			}

			LoggingTarget::Stdout {
				format: LoggingFormat::AnsiNoColor,
			} => {
				stdout_ansi_layer = Some(
					tracing_subscriber::fmt::Layer::default()
						.without_time()
						.with_ansi(false)
						.with_writer(std::io::stdout)
						.with_filter::<EnvFilter>(self.preset.get_config().into()),
				)
			}

			LoggingTarget::Stdout {
				format: LoggingFormat::Json,
			} => {
				stdout_json_layer = Some(
					tracing_subscriber::fmt::Layer::default()
						.without_time()
						.with_ansi(false)
						.json()
						.with_writer(std::io::stdout)
						.with_filter::<EnvFilter>(self.preset.get_config().into()),
				)
			} /*
			  LoggingTarget::Indicatif(mp) => {
				  let writer: IndicatifWriter<tracing_indicatif::writer::Stderr> =
					  IndicatifWriter::new(mp);

				  indicatif_layer = Some(
					  tracing_subscriber::fmt::Layer::default()
						  .without_time()
						  .with_ansi(true)
						  .with_writer(writer.make_writer())
						  .with_filter::<EnvFilter>(self.preset.get_config().into()),
				  )
			  }
			  */
		}

		let loki_layer = {
			#[cfg(feature = "loki")]
			if let Some(cfg) = self.loki {
				use anyhow::Context;
				use base64::{Engine, prelude::BASE64_STANDARD};

				let basic_auth = format!("{}:{}", cfg.loki_user, cfg.loki_pass);
				let encoded_basic_auth = BASE64_STANDARD.encode(basic_auth.as_bytes());

				let (layer, task) = tracing_loki::builder()
					.label("node_name", cfg.loki_node_name)
					.context("while building loki node_name label")?
					.label("app", self.app_name)
					.context("while building loki app label")?
					.http_header("Authorization", format!("Basic {encoded_basic_auth}"))
					.context("while building loki header")?
					.build_url(cfg.loki_host)
					.context("while building loki layer")?;

				tokio::spawn(task);
				Some(layer.with_filter::<EnvFilter>(LogFilterPreset::Loki.get_config().into()))
			} else {
				None
			}

			#[cfg(not(feature = "loki"))]
			None::<Box<dyn Layer<_> + Send + Sync>>
		};

		tracing_subscriber::registry()
			.with(loki_layer)
			.with(stdout_ansi_layer)
			.with(stdout_json_layer)
			.with(stderr_ansi_layer)
			.with(stderr_json_layer)
			.init();

		Ok(())
	}
}
