// =============================================================================
//                                    _
//  |_ _|_ _|_ ._ __ _  ._ _.  _  _ _|_    | __ _ |_     _|_  _|  _       ._
//  | | |_  |_ |_)  (_| | (_| (_ (/_ | |_| |   _> | | |_| |_ (_| (_) \/\/ | |
//             |     _|
// -----------------------------------------------------------------------------
// gracefully shuts downs http server
// can be used with http, express, koa, ...
// (c) 2026 Sebastian Hildebrandt
// License: MIT
// =============================================================================

const debug = require("debug")("http-graceful-shutdown");
const http = require("node:http");

/**
 * Gracefully shuts down `server` when the process receives
 * the passed signals
 *
 * @param {http.Server} server
 * @param {object} opts
 *        signals: string (each signal separated by SPACE)
 *        timeout: timeout value for forceful shutdown in ms
 *        forceExit: force process.exit() - otherwise just let event loop clear
 *        development: boolean value (if true, no graceful shutdown to speed up development
 *        preShutdown: optional function. Needs to return a promise. - HTTP sockets are still available and untouched
 *        onShutdown: optional function. Needs to return a promise.
 *        finally: optional function, handled at the end of the shutdown.
 */

function GracefulShutdown(server, opts) {
	// option handling
	// ----------------------------------
	opts = opts || {};

	// merge opts with default options
	const options = Object.assign(
		{
			signals: "SIGINT SIGTERM",
			timeout: 30000,
			development: false,
			forceExit: true,
			onShutdown: (signal) => Promise.resolve(signal),
			preShutdown: (signal) => Promise.resolve(signal),
		},
		opts,
	);

	let isShuttingDown = false;
	const connections = {};
	let connectionCounter = 0;
	const secureConnections = {};
	let secureConnectionCounter = 0;
	let failed = false;
	let finalRun = false;

	function onceFactory() {
		let called = false;
		return (emitter, events, callback) => {
			function call() {
				if (!called) {
					called = true;
					return callback.apply(this, arguments);
				}
			}
			events.forEach((e) => emitter.on(e, call));
		};
	}

	const signals = options.signals
		.split(" ")
		.map((s) => s.trim())
		.filter((s) => !!s.length);

	const once = onceFactory();

	once(process, signals, (signal) => {
		debug("received shut down signal", signal);
		shutdown(signal)
			.then(() => {
				if (options.forceExit) {
					process.exit(failed ? 1 : 0);
				}
			})
			.catch((err) => {
				debug("server shut down error occurred", err);
				process.exit(1);
			});
	});

	// helper function
	// ----------------------------------
	function isFunction(functionToCheck) {
		const getType = Object.prototype.toString.call(functionToCheck);
		return /^\[object\s([a-zA-Z]+)?Function\]$/.test(getType);
	}

	function destroy(socket, force = false) {
		if ((socket._isIdle && isShuttingDown) || force) {
			socket.destroy();
			if (socket.server instanceof http.Server) {
				delete connections[socket._connectionId];
			} else {
				delete secureConnections[socket._connectionId];
			}
		}
	}

	function destroyAllConnections(force = false) {
		// destroy empty and idle connections / all connections (if force = true)
		debug(`Destroy Connections : ${force ? "forced close" : "close"}`);
		let counter = 0;
		let secureCounter = 0;
		Object.keys(connections).forEach((key) => {
			const socket = connections[key];
			const serverResponse = socket._httpMessage;

			// send connection close header to open connections
			if (serverResponse && !force) {
				if (!serverResponse.headersSent) {
					serverResponse.setHeader("connection", "close");
				}
			} else {
				counter++;
				destroy(socket);
			}
		});

		debug(`Connections destroyed : ${counter}`);
		debug(`Connection Counter    : ${connectionCounter}`);

		Object.keys(secureConnections).forEach((key) => {
			const socket = secureConnections[key];
			const serverResponse = socket._httpMessage;

			// send connection close header to open connections
			if (serverResponse && !force) {
				if (!serverResponse.headersSent) {
					serverResponse.setHeader("connection", "close");
				}
			} else {
				secureCounter++;
				destroy(socket);
			}
		});

		debug(`Secure Connections destroyed : ${secureCounter}`);
		debug(`Secure Connection Counter    : ${secureConnectionCounter}`);
	}

	// set up server/process events
	// ----------------------------------
	server.on("request", (req, res) => {
		req.socket._isIdle = false;
		if (isShuttingDown && !res.headersSent) {
			res.setHeader("connection", "close");
		}

		res.on("finish", () => {
			req.socket._isIdle = true;
			destroy(req.socket);
		});
	});

	server.on("connection", (socket) => {
		if (isShuttingDown) {
			socket.destroy();
		} else {
			const id = connectionCounter++;
			socket._isIdle = true;
			socket._connectionId = id;
			connections[id] = socket;

			socket.once("close", () => {
				delete connections[socket._connectionId];
			});
		}
	});

	server.on("secureConnection", (socket) => {
		if (isShuttingDown) {
			socket.destroy();
		} else {
			const id = secureConnectionCounter++;
			socket._isIdle = true;
			socket._connectionId = id;
			secureConnections[id] = socket;

			socket.once("close", () => {
				delete secureConnections[socket._connectionId];
			});
		}
	});

	process.on("close", () => {
		debug("closed");
	});

	// shutdown event (per signal)
	// ----------------------------------
	function shutdown(sig) {
		function cleanupHttp() {
			destroyAllConnections();
			debug("Close http server");

			return new Promise((resolve, reject) => {
				server.close((err) => {
					if (err) {
						return reject(err);
					}
					return resolve(true);
				});
			});
		}

		debug(`shutdown signal - ${sig}`);

		// Don't bother with graceful shutdown on development to speed up round trip
		if (options.development) {
			debug("DEV-Mode - immediate forceful shutdown");
			return process.exit(0);
		}

		function finalHandler() {
			if (!finalRun) {
				finalRun = true;
				if (options.finally && isFunction(options.finally)) {
					debug("executing finally()");
					options.finally();
				}
			}

			return Promise.resolve();
		}

		// returns true if should force shut down. returns false for shut down without force
		function waitForReadyToShutDown(totalNumInterval) {
			debug(`waitForReadyToShutDown... ${totalNumInterval}`);

			if (totalNumInterval === 0) {
				// timeout reached
				debug(
					`Could not close connections in time (${options.timeout}ms), will forcefully shut down`,
				);
				return Promise.resolve(true);
			}

			// test all connections closed already?
			const allConnectionsClosed =
				Object.keys(connections).length === 0 &&
				Object.keys(secureConnections).length === 0;

			if (allConnectionsClosed) {
				debug("All connections closed. Continue to shutting down");
				return Promise.resolve(false);
			}

			debug("Schedule the next waitForReadyToShutdown");
			return new Promise((resolve) => {
				setTimeout(() => {
					resolve(waitForReadyToShutDown(totalNumInterval - 1));
				}, 250);
			});
		}

		if (isShuttingDown) {
			return Promise.resolve();
		}

		debug("shutting down");

		return options
			.preShutdown(sig)
			.then(() => {
				isShuttingDown = true;
				return cleanupHttp();
			})
			.then(() => {
				const pollIterations = options.timeout
					? Math.round(options.timeout / 250)
					: 0;

				return waitForReadyToShutDown(pollIterations);
			})
			.then((force) => {
				debug("Do onShutdown now");

				// if after waiting for connections to drain within timeout period
				// or if timeout has reached, we forcefully disconnect all sockets
				if (force) {
					destroyAllConnections(force);
				}

				return options.onShutdown(sig);
			})
			.then(finalHandler)
			.catch((err) => {
				const errString = typeof err === "string" ? err : JSON.stringify(err);
				debug(errString);
				failed = true;
				throw errString;
			});
	}

	function shutdownManual() {
		return shutdown("manual");
	}

	return shutdownManual;
}

module.exports = GracefulShutdown;
