use std::path::PathBuf;

use tokio::sync::watch;

use garage_util::background::*;
use garage_util::config::*;
use garage_util::error::Error;

use garage_api::admin::api_server::AdminApiServer;
use garage_api::s3::api_server::S3ApiServer;
use garage_model::garage::Garage;
use garage_web::WebServer;

#[cfg(feature = "k2v")]
use garage_api::k2v::api_server::K2VApiServer;

use crate::admin::*;
#[cfg(feature = "telemetry-otlp")]
use crate::tracing_setup::*;
use crate::{fill_secrets, Secrets};

async fn wait_from(mut chan: watch::Receiver<bool>) {
	while !*chan.borrow() {
		if chan.changed().await.is_err() {
			return;
		}
	}
}

pub async fn run_server(config_file: PathBuf, secrets: Secrets) -> Result<(), Error> {
	info!("Loading configuration...");
	let config = fill_secrets(read_config(config_file)?, secrets);

	// ---- Initialize Garage internals ----

	#[cfg(feature = "metrics")]
	let metrics_exporter = opentelemetry_prometheus::exporter().init();

	info!("Initializing Garage main data store...");
	let garage = Garage::new(config.clone())?;

	info!("Initializing background runner...");
	let watch_cancel = watch_shutdown_signal();
	let (background, await_background_done) = BackgroundRunner::new(watch_cancel.clone());

	info!("Spawning Garage workers...");
	garage.spawn_workers(&background);

	if config.admin.trace_sink.is_some() {
		info!("Initialize tracing...");

		#[cfg(feature = "telemetry-otlp")]
		init_tracing(config.admin.trace_sink.as_ref().unwrap(), garage.system.id)?;

		#[cfg(not(feature = "telemetry-otlp"))]
		error!("Garage was built without OTLP exporter, admin.trace_sink is ignored.");
	}

	info!("Initialize Admin API server and metrics collector...");
	let admin_server = AdminApiServer::new(
		garage.clone(),
		#[cfg(feature = "metrics")]
		metrics_exporter,
	);

	info!("Launching internal Garage cluster communications...");
	let run_system = tokio::spawn(garage.system.clone().run(watch_cancel.clone()));

	info!("Create admin RPC handler...");
	AdminRpcHandler::new(garage.clone(), background.clone());

	// ---- Launch public-facing API servers ----

	let mut servers = vec![];

	if let Some(s3_bind_addr) = &config.s3_api.api_bind_addr {
		info!("Initializing S3 API server...");
		servers.push((
			"S3 API",
			tokio::spawn(S3ApiServer::run(
				garage.clone(),
				s3_bind_addr.clone(),
				config.s3_api.s3_region.clone(),
				wait_from(watch_cancel.clone()),
			)),
		));
	}

	if config.k2v_api.is_some() {
		#[cfg(feature = "k2v")]
		{
			info!("Initializing K2V API server...");
			servers.push((
				"K2V API",
				tokio::spawn(K2VApiServer::run(
					garage.clone(),
					config.k2v_api.as_ref().unwrap().api_bind_addr.clone(),
					config.s3_api.s3_region.clone(),
					wait_from(watch_cancel.clone()),
				)),
			));
		}
		#[cfg(not(feature = "k2v"))]
		error!("K2V is not enabled in this build, cannot start K2V API server");
	}

	if let Some(web_config) = &config.s3_web {
		info!("Initializing web server...");
		servers.push((
			"Web",
			tokio::spawn(WebServer::run(
				garage.clone(),
				web_config.bind_addr.clone(),
				web_config.root_domain.clone(),
				wait_from(watch_cancel.clone()),
			)),
		));
	}

	if let Some(admin_bind_addr) = &config.admin.api_bind_addr {
		info!("Launching Admin API server...");
		servers.push((
			"Admin",
			tokio::spawn(
				admin_server.run(admin_bind_addr.clone(), wait_from(watch_cancel.clone())),
			),
		));
	}

	#[cfg(not(feature = "metrics"))]
	if config.admin.metrics_token.is_some() {
		warn!("This Garage version is built without the metrics feature");
	}

	if servers.is_empty() {
		// Nothing runs except netapp (not in servers)
		// Await shutdown signal before proceeding to shutting down netapp
		wait_from(watch_cancel).await;
	} else {
		// Stuff runs

		// When a cancel signal is sent, stuff stops

		// Collect stuff
		for (desc, join_handle) in servers {
			if let Err(e) = join_handle.await? {
				error!("{} server exited with error: {}", desc, e);
			} else {
				info!("{} server exited without error.", desc);
			}
		}
	}

	// Remove RPC handlers for system to break reference cycles
	info!("Deregistering RPC handlers for shutdown...");
	garage.system.netapp.drop_all_handlers();
	opentelemetry::global::shutdown_tracer_provider();

	// Await for netapp RPC system to end
	run_system.await?;
	info!("Netapp exited");

	// Drop all references so that stuff can terminate properly
	drop(garage);

	// Await for all background tasks to end
	await_background_done.await?;

	info!("Cleaning up...");

	Ok(())
}

#[cfg(unix)]
fn watch_shutdown_signal() -> watch::Receiver<bool> {
	use tokio::signal::unix::*;

	let (send_cancel, watch_cancel) = watch::channel(false);
	tokio::spawn(async move {
		let mut sigint = signal(SignalKind::interrupt()).expect("Failed to install SIGINT handler");
		let mut sigterm =
			signal(SignalKind::terminate()).expect("Failed to install SIGTERM handler");
		let mut sighup = signal(SignalKind::hangup()).expect("Failed to install SIGHUP handler");
		tokio::select! {
			_ = sigint.recv() => info!("Received SIGINT, shutting down."),
			_ = sigterm.recv() => info!("Received SIGTERM, shutting down."),
			_ = sighup.recv() => info!("Received SIGHUP, shutting down."),
		}
		send_cancel.send(true).unwrap();
	});
	watch_cancel
}

#[cfg(windows)]
fn watch_shutdown_signal() -> watch::Receiver<bool> {
	use tokio::signal::windows::*;

	let (send_cancel, watch_cancel) = watch::channel(false);
	tokio::spawn(async move {
		let mut sigint = ctrl_c().expect("Failed to install Ctrl-C handler");
		let mut sigclose = ctrl_close().expect("Failed to install Ctrl-Close handler");
		let mut siglogoff = ctrl_logoff().expect("Failed to install Ctrl-Logoff handler");
		let mut sigsdown = ctrl_shutdown().expect("Failed to install Ctrl-Shutdown handler");
		tokio::select! {
			_ = sigint.recv() => info!("Received Ctrl-C, shutting down."),
			_ = sigclose.recv() => info!("Received Ctrl-Close, shutting down."),
			_ = siglogoff.recv() => info!("Received Ctrl-Logoff, shutting down."),
			_ = sigsdown.recv() => info!("Received Ctrl-Shutdown, shutting down."),
		}
		send_cancel.send(true).unwrap();
	});
	watch_cancel
}