import {
	FModuleVersionGuard,
	FDisposable,
	FExecutionContext,
	FCancellationExecutionContext,
	type FChannelPublisher,
	type FChannelSubscriber,
	FCancellationToken,
	FCancellationTokenSourceManual,
	FException,
	FExceptionInvalidOperation,
	FExceptionAggregate,
	FInitableBase,
	FLogger,
} from "@freemework/common";

import express from 'express';
import fs from "fs";
import net from "net";
import http from "http";
import https from "https";
// import { unescape as urlDecode } from "querystring";
import WebSocket from "ws";
// import { pki } from "node-forge";

import { FHttpRequestCancellationToken } from "./FHttpRequestCancellationToken.js";
import { FHostingSettings } from "./f_hosting_settings.js";

import { packageInfo } from "./package_info.js";
FModuleVersionGuard(packageInfo);

export { FHostingSettings, FHostingConfiguration } from "./f_hosting_settings.js";
export { FHttpRequestCancellationToken } from "./FHttpRequestCancellationToken.js";

export * from "./configuration/index.js";
export * from "./launcher/index.js";

export type FWebServerRequestHandler = http.RequestListener;

export interface FWebServer extends FInitableBase {
	readonly name: string;
	readonly underlyingServer: http.Server | https.Server;
	rootExpressApplication: express.Application;
	bindRequestHandler(bindPath: string, handler: FWebServerRequestHandler): void;
	createWebSocketServer(bindPath: string): WebSocket.Server;
	destroyWebSocketServer(bindPath: string): Promise<void>;
}

export abstract class FAbstractWebServer<TOpts extends FHostingSettings.WebServerBase | FHostingSettings.WebServer>
	extends FInitableBase implements FWebServer {
	public abstract readonly underlyingServer: http.Server | https.Server;
	protected readonly _trustProxy: boolean | "loopback" | "linklocal" | "uniquelocal";
	protected readonly _name: string;
	protected readonly _listenHost: string;
	protected readonly _listenPort: number;
	protected readonly _webSockets: { [bindPath: string]: WebSocket.Server };
	protected readonly _log: FLogger;
	protected readonly _handleUpgrade: boolean;
	private readonly _onUpgrade: ((request: http.IncomingMessage, socket: net.Socket, head: Buffer) => void) | null;
	private readonly _onRequestImpl: http.RequestListener;
	private readonly _handlers: Map</*bindPath: */string, FWebServerRequestHandler>;
	// private readonly _caCertificates: ReadonlyArray<[pki.Certificate, Buffer]>;
	private _rootExpressApplication: express.Application | null;

	public static createFCancellationToken(request: http.IncomingMessage): FCancellationToken {
		return new FHttpRequestCancellationToken(request);
	}

	public constructor(opts: TOpts) {
		super();
		this._name = opts.name;
		this._listenHost = opts.listenHost;
		this._listenPort = opts.listenPort;
		this._log = opts.log !== undefined ? opts.log : FLogger.create(this.constructor.name);
		this._trustProxy = opts.trustProxy !== undefined ? opts.trustProxy : false;
		this._handleUpgrade = opts.handleUpgrade !== undefined ? opts.handleUpgrade : true;
		this._webSockets = {};
		this._handlers = new Map();
		this._rootExpressApplication = null;

		let onXfccRequest: http.RequestListener | null = null;
		let onXfccUpgrade: ((request: http.IncomingMessage, socket: net.Socket, head: Buffer) => void) | null = null;
		if ("type" in opts) {
			const friendlyOpts = opts as FHostingSettings.WebServer;
			if (
				"clientCertificateMode" in friendlyOpts &&
				friendlyOpts.clientCertificateMode === FHostingSettings.ClientCertificateMode.XFCC
			) {
				if (
					friendlyOpts.caCertificates === undefined ||
					!(
						typeof friendlyOpts.caCertificates === "string"
						|| friendlyOpts.caCertificates instanceof Buffer
						|| Array.isArray(friendlyOpts.caCertificates)
					)
				) {
					throw new Error("ClientCertificateMode.XFCC required at least one CA certificate");
				}

				// this._caCertificates = parseCertificates(friendlyOpts.caCertificates);

				onXfccRequest = this.onRequestXFCC.bind(this);
				onXfccUpgrade = this.onUpgradeXFCC.bind(this);
			} else {
				// if (
				// 	friendlyOpts.type === "https" &&
				// 	friendlyOpts.caCertificates !== undefined &&
				// 	(
				// 		typeof friendlyOpts.caCertificates === "string"
				// 		|| friendlyOpts.caCertificates instanceof Buffer
				// 		|| Array.isArray(friendlyOpts.caCertificates)
				// 	)
				// ) {
				// 	this._caCertificates = parseCertificates(friendlyOpts.caCertificates);
				// } else {
				// 	this._caCertificates = [];
				// }
			}
		} else {
			// this._caCertificates = [];
		}

		this._onRequestImpl = onXfccRequest !== null ? onXfccRequest : this.onRequestCommon.bind(this);
		this._onUpgrade = this._handleUpgrade
			? (onXfccUpgrade !== null
				? onXfccUpgrade
				: this.onUpgradeCommon.bind(this)
			)
			: null;
	}

	/**
	 * Lazy create for Express Application
	 */
	public get rootExpressApplication(): express.Application {
		if (this._rootExpressApplication === null) {
			this._rootExpressApplication = express();
			const trustProxy = this._trustProxy;
			if (trustProxy !== undefined) {
				this._rootExpressApplication.set("trust proxy", trustProxy);
			}
		}
		return this._rootExpressApplication;
	}

	public set rootExpressApplication(value: express.Application) {
		if (this._rootExpressApplication !== null) {
			throw new Error("Wrong operation at current state. Express application already set. Override is not allowed.");
		}
		this._rootExpressApplication = value;
	}

	public get name(): string { return this._name; }

	public bindRequestHandler(bindPath: string, value: FWebServerRequestHandler): void {
		if (this._handlers.has(bindPath)) {
			throw new Error(`Wrong operation. Path '${bindPath}' already bound`);
		}
		this._handlers.set(bindPath, value);
	}

	public createWebSocketServer(bindPath: string): WebSocket.Server {
		const websocketServer: WebSocket.Server = new WebSocket.Server({ noServer: true });
		this._webSockets[bindPath] = websocketServer;
		return websocketServer;
	}
	public async destroyWebSocketServer(bindPath: string): Promise<void> {
		const logger: FLogger = this._log;

		const webSocketServer = this._webSockets[bindPath];
		if (webSocketServer !== undefined) {
			delete this._webSockets[bindPath];
			await new Promise<void>((resolve) => {
				webSocketServer.close((err) => {
					if (err !== undefined) {
						if (logger.isWarnEnabled) {
							logger.warn(this.initExecutionContext, () => `Web Socket Server was closed with error. Inner message: ${err.message} `);
						}
						logger.trace(this.initExecutionContext, () => "Web Socket Server was closed with error.", FException.wrapIfNeeded(err));
					}

					// dispose never raise any errors
					resolve();
				});
			});
		}
	}

	protected async onInit(): Promise<void> {
		if (this._onUpgrade !== null) {
			this.underlyingServer.on("upgrade", this._onUpgrade);
		}
		await this.onListen();
	}

	// protected get caCertificatesAsPki(): Array<pki.Certificate> {
	// 	if (this._caCertificates === undefined) {
	// 		throw new Error("Wrong operation at current state.");
	// 	}
	// 	return this._caCertificates.map(tuple => tuple[0]);
	// }

	// protected get caCertificatesAsBuffer(): Array<Buffer> {
	// 	if (this._caCertificates === undefined) {
	// 		throw new Error("Wrong operation at current state.");
	// 	}
	// 	return this._caCertificates.map(tuple => tuple[1]);
	// }

	protected abstract onListen(): Promise<void>;

	protected onRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
		this._onRequestImpl(req, res);
	}

	private onRequestCommon(req: http.IncomingMessage, res: http.ServerResponse): void {
		const logger: FLogger = this._log;

		if (this._handlers.size > 0 && req.url !== undefined) {
			const { pathname } = new URL(req.url);
			if (pathname !== undefined) {
				for (const bindPath of this._handlers.keys()) {
					if (pathname.startsWith(bindPath)) {
						const handler = this._handlers.get(bindPath) as FWebServerRequestHandler;
						handler(req, res);
						return;
					}
				}
			}
		}

		// A proper handler was not found, fallback to rootExpressApplication
		if (this._rootExpressApplication !== null) {
			this._rootExpressApplication(req, res);
			return;
		}

		logger.warn(this.initExecutionContext, "Request was handled but no listener.");
		res.writeHead(503);
		res.statusMessage = "Service Unavailable";
		res.end();
	}

	private onRequestXFCC(req: http.IncomingMessage, res: http.ServerResponse): void {
		if (this.validateXFCC(req)) {
			this.onRequestCommon(req, res);
			return;
		}

		res.statusMessage = "Client certificate is required.";
		res.writeHead(401);
		res.end();
	}

	private onUpgradeCommon(req: http.IncomingMessage, socket: net.Socket, head: Buffer): void {
		const logger: FLogger = this._log;

		const urlPath = req.url;
		if (urlPath !== undefined) {
			const wss = this._webSockets[urlPath];
			if (wss !== undefined) {
				logger.debug(this.initExecutionContext, () => `Upgrade the server on url path '${urlPath}' for WebSocket server.`);
				wss.handleUpgrade(req, socket, head, function (ws) {
					wss.emit("connection", ws, req);
				});
			} else {
				// Proposal of https://github.com/freemework/freemework/issues/8
				// socket.destroy();
			}
		} else {
			// Proposal of https://github.com/freemework/freemework/issues/8
			// socket.destroy();
		}
	}

	private onUpgradeXFCC(req: http.IncomingMessage, socket: net.Socket, head: Buffer): void {
		if (this.validateXFCC(req)) {
			this.onUpgradeCommon(req, socket, head);
			return;
		}

		socket.write("HTTP/1.1 401 Client certificate is required.\r\n\r\n");
		socket.end();
	}

	private validateXFCC(req: http.IncomingMessage): boolean {
		const logger: FLogger = this._log;

		const xfccHeaderData = req && req.headers && req.headers["x-forwarded-client-cert"];
		if (typeof xfccHeaderData === "string") {
			logger.trace(this.initExecutionContext, () => `X-Forwarded-Client-Cert header: ${xfccHeaderData}`);

			// const clientCertPem = urlDecode(xfccHeaderData);
			// const clientCert = pki.certificateFromPem(clientCertPem);

			// for (const caCert of this.caCertificatesAsPki) {
			// 	try {
			// 		if (caCert.verify(clientCert)) {
			// 			return true;
			// 		}
			// 	} catch (e) {
			// 		logger.trace(this.initExecutionContext, "Verify failed.", FException.wrapIfNeeded(e));
			// 	}
			// }
		} else {
			logger.debug(this.initExecutionContext, "Request with no X-Forwarded-Client-Cert header.");
		}

		return false;
	}
}

export class UnsecuredWebServer extends FAbstractWebServer<FHostingSettings.UnsecuredWebServer> {
	private readonly _httpServer: http.Server;

	public constructor(opts: FHostingSettings.UnsecuredWebServer) {
		super(opts);

		// Make HTTP server instance
		const serverOpts: https.ServerOptions = {
		};

		this._httpServer = http.createServer(serverOpts, this.onRequest.bind(this));
	}

	public get underlyingServer(): http.Server { return this._httpServer; }

	protected onListen(): Promise<void> {
		const logger: FLogger = this._log;

		logger.debug(this.initExecutionContext, "UnsecuredWebServer#listen()");

		const server: http.Server = this._httpServer;
		return new Promise<void>((resolve, reject) => {
			logger.info(this.initExecutionContext, () => `Starting Web Server '${this._name}' ...`);
			server
				.on("listening", () => {
					const address = server.address();
					if (address !== null) {
						if (typeof address === "string") {
							logger.info(this.initExecutionContext, () => `Web Server '${this._name}' was started on ${address}`);
						} else {
							logger.info(this.initExecutionContext, () => `${address.family} Web Server '${this._name}' was started on http://${address.address}:${address.port}`);
						}
					}
					resolve();
				})
				.on("error", reject)
				.listen(this._listenPort, this._listenHost);
		});
	}

	protected async onDispose() {
		const logger: FLogger = this._log;
		logger.debug(this.initExecutionContext, "UnsecuredWebServer#onDispose()");
		const server = this._httpServer;
		const address = server.address();
		if (address !== null) {
			if (typeof address === "string") {
				logger.info(this.initExecutionContext, () => `Stopping Web Server '${this._name}' http://${address}...`);
			} else {
				logger.info(this.initExecutionContext, () => `Stopping ${address.family} Web Server '${this._name}' http://${address.address}:${address.port}...`);
			}
		} else {
			logger.info(this.initExecutionContext, "Stopping Web Server...");
		}
		await new Promise<void>((destroyResolve) => {
			server.close((e) => {
				if (e) {
					const ex: FException = FException.wrapIfNeeded(e);
					logger.warn(this.initExecutionContext, () => `The Web Server '${this._name}' was stopped with error: ${ex.message}`);
					logger.debug(this.initExecutionContext, () => `The Web Server '${this._name}' was stopped with error`, ex);
				} else {
					logger.info(this.initExecutionContext, "The Web Server was stopped");
				}
				destroyResolve();
			});
		});
	}
}

export class SecuredWebServer extends FAbstractWebServer<FHostingSettings.SecuredWebServer> {
	private readonly _httpsServer: https.Server;

	public constructor(opts: FHostingSettings.SecuredWebServer) {
		super(opts);

		// Make HTTPS server instance
		const serverOpts: https.ServerOptions = {
			cert: opts.serverCertificate instanceof Buffer ? opts.serverCertificate : fs.readFileSync(opts.serverCertificate),
			key: opts.serverKey instanceof Buffer ? opts.serverKey : fs.readFileSync(opts.serverKey)
		};

		if (opts.caCertificates !== null) {
			if (typeof opts.caCertificates === "string") {
				serverOpts.ca = fs.readFileSync(opts.caCertificates);
			}
			//serverOpts.ca = this.caCertificatesAsBuffer;
		}
		if (opts.serverKeyPassword !== null) {
			serverOpts.passphrase = opts.serverKeyPassword;
		}

		switch (opts.clientCertificateMode) {
			case FHostingSettings.ClientCertificateMode.NONE:
			case FHostingSettings.ClientCertificateMode.XFCC: // XFCC handled by AbstractWebServer
				serverOpts.requestCert = false;
				serverOpts.rejectUnauthorized = false;
				break;
			case FHostingSettings.ClientCertificateMode.REQUEST:
				serverOpts.requestCert = true;
				serverOpts.rejectUnauthorized = false;
				break;
			default:
				// By default use FHostingConfiguration.SecuredWebServer.ClientCertMode.TRUST mode
				serverOpts.requestCert = true;
				serverOpts.rejectUnauthorized = true;
				break;
		}

		this._httpsServer = https.createServer(serverOpts, this.onRequest.bind(this));
	}

	public get underlyingServer(): https.Server { return this._httpsServer; }

	protected onListen(): Promise<void> {
		const logger: FLogger = this._log;
		logger.debug(this.initExecutionContext, "SecuredWebServer#listen()");

		const server: https.Server = this._httpsServer;
		return new Promise((resolve, reject) => {
			logger.info(this.initExecutionContext, () => `Starting Web Server '${this._name}' ...`);
			server
				.on("listening", () => {
					const address = server.address();
					if (address !== null) {
						if (typeof address === "string") {
							logger.info(this.initExecutionContext, () => `Web Server '${this._name}' was started on ${address}`);
						} else {
							logger.info(this.initExecutionContext, () => `${address.family} Web Server '${this._name}' was started on https://${address.address}:${address.port}`);
						}
					}
					resolve();
				})
				.on("error", reject)
				.listen(this._listenPort, this._listenHost);
		});
	}

	protected async onDispose() {
		const logger: FLogger = this._log;
		logger.debug(this.initExecutionContext, "SecuredWebServer#onDispose()");
		const server = this._httpsServer;
		logger.info(this.initExecutionContext, () => `Stopping Web Server  '${this._name}' ...`);
		await new Promise<void>((destroyResolve) => {
			server.close((e) => {
				if (e) {
					const ex: FException = FException.wrapIfNeeded(e);
					logger.warn(this.initExecutionContext, () => `The Web Server '${this._name}' was stopped with error: ${ex.message}`);
					logger.debug(this.initExecutionContext, () => `The Web Server '${this._name}' was stopped with error`, ex);
				} else {
					logger.info(this.initExecutionContext, () => `The Web Server '${this._name}' was stopped`);
				}
				destroyResolve();
			});
		});
	}
}

export abstract class FBindEndpoint extends FInitableBase {
	protected readonly _bindPath: string;

	public constructor(
		opts: FHostingSettings.BindEndpoint
	) {
		super();
		this._bindPath = opts.bindPath;
	}

	protected onInit(): void | Promise<void> {
		// NOP
	}

	protected onDispose(): void | Promise<void> {
		// NOP
	}
}

export abstract class FServersBindEndpoint extends FBindEndpoint {
	protected readonly _servers: ReadonlyArray<FWebServer>;

	public constructor(
		servers: ReadonlyArray<FWebServer>,
		opts: FHostingSettings.BindEndpoint
	) {
		super(opts);
		this._servers = servers;
	}
}

/**
 * This endpoint supplies communication channels for each client connection.
 *
 * WebSocket Client is hosting the channel:
 *   - Client's messages delivered via SubscriberChannel
 *   - To deliver message to client use PublisherChannel
 *
 * If you need opposite behavior take a look for `WebSocketChannelFactoryEndpoint`
 *
 * You need to override onOpenBinaryChannel and/or onOpenTextChannel to obtain necessary channel
 */
export class FWebSocketChannelSupplyEndpoint extends FServersBindEndpoint {
	private readonly _webSocketServers: Array<WebSocket.Server>;
	private readonly _connections: Set<WebSocket>;
	private readonly _log: FLogger;
	private _defaultProtocol: string;
	private _allowedProtocols: Set<string>;
	private _connectionCounter: number;

	public constructor(
		servers: ReadonlyArray<FWebServer>,
		opts: FHostingSettings.WebSocketEndpoint
	) {
		super(servers, opts);
		this._log = FLogger.create(this.constructor.name);
		this._webSocketServers = [];
		this._connections = new Set();
		this._defaultProtocol = opts.defaultProtocol;
		this._allowedProtocols = new Set([this._defaultProtocol]);
		if (opts.allowedProtocols !== undefined) {
			opts.allowedProtocols.forEach((allowedProtocol: string): void => {
				this._allowedProtocols.add(allowedProtocol);
			});
		}
		this._connectionCounter = 0;
	}

	protected override onInit(): void {
		super.onInit();

		for (const server of this._servers) {
			const webSocketServer = server.createWebSocketServer(this._bindPath); // new WebSocket.Server({ noServer: true });
			this._webSocketServers.push(webSocketServer);
			webSocketServer.on("connection", this.onConnection.bind(this));
		}
	}

	protected override async onDispose() {
		const logger: FLogger = this._log;

		const connections = [...this._connections.values()];
		this._connections.clear();
		for (const webSocket of connections) {
			webSocket.close(1001, "going away");
			webSocket.terminate();
		}

		const webSocketServers = this._webSocketServers.splice(0).reverse();
		for (const webSocketServer of webSocketServers) {
			await new Promise<void>((resolve) => {
				webSocketServer.close((err) => {
					if (err !== undefined) {
						logger.warn(this.initExecutionContext, () => `Web Socket Server was closed with error. Inner message: ${err.message} `);
						logger.trace(this.initExecutionContext, "Web Socket Server was closed with error.", FException.wrapIfNeeded(err));
					}

					// dispose never raise any errors
					resolve();
				});
			});
		}
	}

	protected onConnection(webSocket: WebSocket, request: http.IncomingMessage): void {
		if (this.disposing) {
			// https://tools.ietf.org/html/rfc6455#section-7.4.1
			webSocket.close(1001, "going away");
			webSocket.terminate();
			return;
		}

		const logger: FLogger = this._log;

		if (this._connectionCounter === Number.MAX_SAFE_INTEGER) { this._connectionCounter = 0; }
		const connectionNumber: number = this._connectionCounter++;
		const ipAddress: string | undefined = request.socket.remoteAddress;
		if (ipAddress !== undefined) {
			logger.info(this.initExecutionContext, () => `Connection #${connectionNumber} was established from ${ipAddress} `);
		}
		if (logger.isInfoEnabled) {
			logger.info(this.initExecutionContext, `Connection #${connectionNumber} was established`);
		}

		const subProtocol: string = webSocket.protocol || this._defaultProtocol;
		if (webSocket.protocol !== undefined) {
			if (!this._allowedProtocols.has(subProtocol)) {
				logger.warn(this.initExecutionContext, () => `Connection #${connectionNumber} dropped. Not supported sub-protocol: ${subProtocol}`);
				// https://tools.ietf.org/html/rfc6455#section-7.4.1
				webSocket.close(1007, `Wrong sub-protocol: ${subProtocol}`);
				webSocket.terminate();
				return;
			}
		}

		const FCancellationTokenSource = new FCancellationTokenSourceManual();

		webSocket.binaryType = "nodebuffer";

		const channels: Map</*protocol:*/ string, {
			binaryChannel?: _FWebSocketChannelSupplyEndpointHelpers.WebSocketBinaryChannelImpl,
			textChannel?: _FWebSocketChannelSupplyEndpointHelpers.WebSocketTextChannelImpl
		}> = new Map();

		webSocket.onmessage = async ({ data }) => {
			try {
				let channelsTuple = channels.get(subProtocol);
				if (channelsTuple === undefined) {
					channelsTuple = {};
					channels.set(subProtocol, channelsTuple);
				}

				if (data instanceof Buffer) {
					if (channelsTuple.binaryChannel === undefined) {
						const binaryChannel = new _FWebSocketChannelSupplyEndpointHelpers.WebSocketBinaryChannelImpl(webSocket);
						try {
							this.onOpenBinaryChannel(webSocket, subProtocol, binaryChannel);
						} catch (e) {
							// https://tools.ietf.org/html/rfc6455#section-7.4.1
							const friendlyError: FException = FException.wrapIfNeeded(e);
							webSocket.close(1011, friendlyError.message);
							webSocket.terminate();
							return;
						}
						channelsTuple.binaryChannel = binaryChannel;
					}
					await channelsTuple.binaryChannel.onMessage(FCancellationTokenSource.token, data);
				} else if (typeof data === "string") {
					if (channelsTuple.textChannel === undefined) {
						const textChannel = new _FWebSocketChannelSupplyEndpointHelpers.WebSocketTextChannelImpl(webSocket);
						try {
							this.onOpenTextChannel(webSocket, subProtocol, textChannel);
						} catch (e) {
							// https://tools.ietf.org/html/rfc6455#section-7.4.1
							const friendlyError: FException = FException.wrapIfNeeded(e);
							webSocket.close(1011, friendlyError.message);
							webSocket.terminate();
							return;
						}
						channelsTuple.textChannel = textChannel;
					}
					await channelsTuple.textChannel.onMessage(FCancellationTokenSource.token, data);
				} else {
					logger.debug(
						this.initExecutionContext,
						() => `Connection #${connectionNumber} cannot handle a message due not supported type. Terminate socket...`
					);

					// https://tools.ietf.org/html/rfc6455#section-7.4.1
					webSocket.close(1003, `Not supported message type`);
					webSocket.terminate();
					return;
				}
			} catch (e) {
				if (logger.isInfoEnabled || logger.isTraceEnabled) {
					const ex: FException = FException.wrapIfNeeded(e);
					logger.info(this.initExecutionContext, () => `Connection #${connectionNumber} onMessage failed: ${ex.message}`);
					logger.trace(this.initExecutionContext, () => `Connection #${connectionNumber} onMessage failed:`, ex);
				}
			}
		};
		webSocket.onclose = ({ code, reason }) => {
			logger.trace(this.initExecutionContext, () => `Connection #${connectionNumber} was closed: ${JSON.stringify({ code, reason })} `);
			logger.info(this.initExecutionContext, () => `Connection #${connectionNumber} was closed`);

			FCancellationTokenSource.cancel();
			this._connections.delete(webSocket);

			const closedError = new FException(`WebSocket was closed: ${code} ${reason}`);
			for (const channelsTuple of channels.values()) {
				if (channelsTuple.binaryChannel !== undefined) {
					channelsTuple.binaryChannel.onClose(closedError).catch(console.error);
				}
				if (channelsTuple.textChannel !== undefined) {
					channelsTuple.textChannel.onClose(closedError).catch(console.error);
				}
			}
			channels.clear();
		};

		this._connections.add(webSocket);
	}

	/**
	 * The method should be overridden. The method called by the endpoint,
	 * when WSClient sent first binary message for specified sub-protocol.
	 * @param webSocket WebSocket instance
	 * @param subProtocol These strings are used to indicate sub-protocols,
	 * so that a single server can implement multiple WebSocket sub-protocols (for example,
	 * you might want one server to be able to handle different types of interactions
	 * depending on the specified protocol).
	 * @param channel Binary channel instance to be user in inherited class
	 */
	protected onOpenBinaryChannel(_webSocket: WebSocket, subProtocol: string, _channel: FWebSocketChannelSupplyEndpoint.BinaryChannel): void {
		throw new FExceptionInvalidOperation(`Binary messages are not supported by the sub-protocol: ${subProtocol}`);
	}

	/**
	 * The method should be overridden. The method called by the endpoint,
	 * when WSClient sent first text message for specified sub-protocol.
	 * @param webSocket WebSocket instance
	 * @param subProtocol These strings are used to indicate sub-protocols,
	 * so that a single server can implement multiple WebSocket sub-protocols (for example,
	 * you might want one server to be able to handle different types of interactions
	 * depending on the specified protocol).
	 * @param channel Text channel instance to be user in inherited class
	 */
	protected onOpenTextChannel(_webSocket: WebSocket, subProtocol: string, _channel: FWebSocketChannelSupplyEndpoint.TextChannel): void {
		throw new FExceptionInvalidOperation(`Text messages are not supported by the sub-protocol: ${subProtocol}`);
	}
}
export namespace FWebSocketChannelSupplyEndpoint {
	export interface BinaryChannel extends FChannelPublisher<Uint8Array>, FChannelSubscriber<Uint8Array> { }
	export interface TextChannel extends FChannelPublisher<string>, FChannelSubscriber<string> { }
}

/**
 * This endpoint requests you for communication channels for each client connection.
 *
 * WebSocket Client is used the channel:
 *   - Client's messages delivered via PublisherChannel
 *   - To deliver message to client use SubscriberChannel
 *
 * If you need opposite behavior take a look for `WebSocketChannelSupplyEndpoint`
 *
 * You need to override createBinaryChannel and/or createTextChannel to provide necessary channel
 */
export class FWebSocketChannelFactoryEndpoint extends FServersBindEndpoint {
	private readonly _webSocketServers: Array<WebSocket.Server>;
	private readonly _connections: Set<WebSocket>;
	private readonly _autoCreateChannelBinary: boolean;
	private readonly _autoCreateChannelText: boolean;
	private readonly _log: FLogger;
	private _defaultProtocol: string;
	private _allowedProtocols: Set<string>;
	private _connectionCounter: number;

	public constructor(
		servers: ReadonlyArray<FWebServer>,
		opts: FHostingSettings.WebSocketEndpoint,
		autoCreateChannels?: {
			binary?: boolean,
			text?: boolean
		}
	) {
		super(servers, opts);
		this._log = FLogger.create(this.constructor.name);
		this._webSocketServers = [];
		this._connections = new Set();
		this._defaultProtocol = opts.defaultProtocol;
		this._allowedProtocols = new Set([this._defaultProtocol]);
		if (opts.allowedProtocols !== undefined) {
			opts.allowedProtocols.forEach((allowedProtocol: string): void => {
				this._allowedProtocols.add(allowedProtocol);
			});
		}
		this._connectionCounter = 0;

		this._autoCreateChannelBinary = false;
		this._autoCreateChannelText = false;

		if (autoCreateChannels !== undefined) {
			if (autoCreateChannels.binary === true) { this._autoCreateChannelBinary = true; }
			if (autoCreateChannels.text === true) { this._autoCreateChannelText = true; }
		}
	}

	protected override onInit(): void {
		super.onInit();

		for (const server of this._servers) {
			const webSocketServer = server.createWebSocketServer(this._bindPath); // new WebSocket.Server({ noServer: true });
			this._webSocketServers.push(webSocketServer);
			webSocketServer.on("connection", this.onConnection.bind(this));
		}
	}

	protected override async onDispose() {
		// Prevent open new connection
		for (const server of this._servers) {
			await server.destroyWebSocketServer(this._bindPath);
		}

		const connections = [...this._connections.values()];
		this._connections.clear();
		for (const webSocket of connections) {
			webSocket.close(1001, "going away");
			webSocket.terminate();
		}
		this._webSocketServers.splice(0);
	}

	protected async onConnection(webSocket: WebSocket, request: http.IncomingMessage): Promise<void> {
		if (this.disposing) {
			// https://tools.ietf.org/html/rfc6455#section-7.4.1
			webSocket.close(1001, "going away");
			webSocket.terminate();
			return;
		}

		const logger: FLogger = this._log;

		if (this._connectionCounter === Number.MAX_SAFE_INTEGER) { this._connectionCounter = 0; }
		const connectionNumber: number = this._connectionCounter++;
		const ipAddress: string | undefined = request.connection.remoteAddress;
		if (ipAddress !== undefined) {
			logger.info(this.initExecutionContext, () => `Connection #${connectionNumber} was established from ${ipAddress} `);
		} else {
			logger.info(this.initExecutionContext, () => `Connection #${connectionNumber} was established`);
		}

		const subProtocol: string = webSocket.protocol || this._defaultProtocol;
		if (webSocket.protocol !== undefined) {
			if (!this._allowedProtocols.has(subProtocol)) {
				logger.warn(this.initExecutionContext, `Connection #${connectionNumber} dropped. Not supported sub-protocol: ${subProtocol}`);
				// https://tools.ietf.org/html/rfc6455#section-7.4.1
				webSocket.close(1007, `Wrong sub-protocol: ${subProtocol}`);
				webSocket.terminate();
				return;
			}
		}

		const cancellationTokenSource: FCancellationTokenSourceManual = new FCancellationTokenSourceManual();

		const executionContext: FExecutionContext = new FCancellationExecutionContext(
			FExecutionContext.Empty,
			cancellationTokenSource.token
		);

		webSocket.binaryType = "nodebuffer";

		const channels: Map</*protocol:*/ string, {
			binaryChannel?: FWebSocketChannelFactoryEndpoint.BinaryChannel,
			textChannel?: FWebSocketChannelFactoryEndpoint.TextChannel
		}> = new Map();

		const handler = async (
			_executionContext: FExecutionContext,
			event: FChannelSubscriber.Event<Uint8Array> | FChannelSubscriber.Event<string> | FException
		) => {
			if (event instanceof FException) {
				// https://tools.ietf.org/html/rfc6455#section-7.4.1
				logger.debug(this.initExecutionContext, "Channel emits error. " + event.message);
				logger.trace(this.initExecutionContext, "Channel emits error.", event);
				webSocket.close(1011, event.message);
				webSocket.terminate();
			} else {
				const { data } = event;
				await new Promise<void>(sendResolve => webSocket.send(data, () => sendResolve()));
			}
		};

		if (this._autoCreateChannelBinary === true) {
			let channelsTuple = channels.get(subProtocol);
			if (channelsTuple === undefined) {
				channelsTuple = {};
				channels.set(subProtocol, channelsTuple);
			}
			try {
				const binaryChannel: FWebSocketChannelFactoryEndpoint.BinaryChannel
					= await this.createBinaryChannel(executionContext, webSocket, subProtocol);
				channelsTuple.binaryChannel = binaryChannel;
				binaryChannel.addHandler(handler);
			} catch (e) {
				// https://tools.ietf.org/html/rfc6455#section-7.4.1
				const friendlyError: FException = FException.wrapIfNeeded(e);
				logger.debug(this.initExecutionContext, () => `Could not create binary channel. ${friendlyError.message}`);
				logger.trace(this.initExecutionContext, "Could not create binary channel.", friendlyError);
				webSocket.close(1011, friendlyError.message);
				webSocket.terminate();
				return;
			}
		}

		if (this._autoCreateChannelText === true) {
			let channelsTuple = channels.get(subProtocol);
			if (channelsTuple === undefined) {
				channelsTuple = {};
				channels.set(subProtocol, channelsTuple);
			}
			try {
				const textChannel: FWebSocketChannelFactoryEndpoint.TextChannel
					= await this.createTextChannel(executionContext, webSocket, subProtocol);
				channelsTuple.textChannel = textChannel;
				textChannel.addHandler(handler);
			} catch (e) {
				// https://tools.ietf.org/html/rfc6455#section-7.4.1
				const friendlyError: FException = FException.wrapIfNeeded(e);
				logger.debug(this.initExecutionContext, () => `Could not create text channel. ${friendlyError.message}`);
				logger.trace(this.initExecutionContext, "Could not create text channel.", friendlyError);
				webSocket.close(1011, friendlyError.message);
				webSocket.terminate();
				return;
			}
		}

		webSocket.onmessage = async ({ data }) => {
			try {
				let channelsTuple = channels.get(subProtocol);
				if (channelsTuple === undefined) {
					channelsTuple = {};
					channels.set(subProtocol, channelsTuple);
				}

				if (data instanceof Buffer) {
					if (channelsTuple.binaryChannel === undefined) {
						try {
							const binaryChannel: FWebSocketChannelFactoryEndpoint.BinaryChannel
								= await this.createBinaryChannel(executionContext, webSocket, subProtocol);
							channelsTuple.binaryChannel = binaryChannel;
							binaryChannel.addHandler(handler);
						} catch (e) {
							// https://tools.ietf.org/html/rfc6455#section-7.4.1
							const friendlyError: FException = FException.wrapIfNeeded(e);
							logger.debug(this.initExecutionContext, () => `Could not create binary channel. ${friendlyError.message}`);
							logger.trace(this.initExecutionContext, "Could not create binary channel.", friendlyError);
							webSocket.close(1011, friendlyError.message);
							webSocket.terminate();
							return;
						}
					}
					await channelsTuple.binaryChannel.send(FExecutionContext.Empty, data);
				} else if (typeof data === "string") {
					if (channelsTuple.textChannel === undefined) {
						try {
							const textChannel: FWebSocketChannelFactoryEndpoint.TextChannel
								= await this.createTextChannel(executionContext, webSocket, subProtocol);
							channelsTuple.textChannel = textChannel;
							textChannel.addHandler(handler);
						} catch (e) {
							// https://tools.ietf.org/html/rfc6455#section-7.4.1
							const friendlyError: FException = FException.wrapIfNeeded(e);
							logger.debug(this.initExecutionContext, () => `Could not create text channel. ${friendlyError.message}`);
							logger.trace(this.initExecutionContext, "Could not create text channel.", friendlyError);
							webSocket.close(1011, friendlyError.message);
							webSocket.terminate();
							return;
						}
					}
					await channelsTuple.textChannel.send(FExecutionContext.Empty, data);
				} else {
					logger.debug(
						this.initExecutionContext,
						() => `Connection #${connectionNumber} cannot handle a message due not supported type. Terminate socket...`
					);

					// https://tools.ietf.org/html/rfc6455#section-7.4.1
					webSocket.close(1003, `Not supported message type`);
					webSocket.terminate();
					return;
				}
			} catch (e) {
				const ex: FException = FException.wrapIfNeeded(e);
				logger.info(this.initExecutionContext, () => `Connection #${connectionNumber} onMessage failed: ${ex.message}`);
				logger.trace(this.initExecutionContext, () => `Connection #${connectionNumber} onMessage failed:`, ex);
			}
		};
		webSocket.onclose = ({ code, reason }) => {
			logger.trace(this.initExecutionContext, () => `Connection #${connectionNumber} was closed: ${JSON.stringify({ code, reason })} `);
			logger.info(this.initExecutionContext, () => `Connection #${connectionNumber} was closed`);

			cancellationTokenSource.cancel();
			this._connections.delete(webSocket);

			for (const channelsTuple of channels.values()) {
				if (channelsTuple.binaryChannel !== undefined) {
					channelsTuple.binaryChannel.removeHandler(handler);
					channelsTuple.binaryChannel.dispose().catch(console.error);
				}
				if (channelsTuple.textChannel !== undefined) {
					channelsTuple.textChannel.removeHandler(handler);
					channelsTuple.textChannel.dispose().catch(console.error);
				}
			}
			channels.clear();
		};

		this._connections.add(webSocket);
	}

	/**
	 * The method should be overridden. The method called by the endpoint,
	 * when WSClient sent first binary message for specified sub-protocol.
	 * @param webSocket WebSocket instance
	 * @param subProtocol These strings are used to indicate sub-protocols,
	 * so that a single server can implement multiple WebSocket sub-protocols (for example,
	 * you might want one server to be able to handle different types of interactions
	 * depending on the specified protocol).
	 */
	protected createBinaryChannel(
		_executionContext: FExecutionContext, _webSocket: WebSocket, subProtocol: string
	): Promise<FWebSocketChannelFactoryEndpoint.BinaryChannel> {
		throw new FExceptionInvalidOperation(`Binary messages are not supported by the sub-protocol: ${subProtocol}`);
	}

	/**
	 * The method should be overridden. The method called by the endpoint,
	 * when WSClient sent first text message for specified sub-protocol.
	 * @param webSocket WebSocket instance
	 * @param subProtocol These strings are used to indicate sub-protocols,
	 * so that a single server can implement multiple WebSocket sub-protocols (for example,
	 * you might want one server to be able to handle different types of interactions
	 * depending on the specified protocol).
	 */
	protected createTextChannel(
		_executionContext: FExecutionContext, _webSocket: WebSocket, subProtocol: string
	): Promise<FWebSocketChannelFactoryEndpoint.TextChannel> {
		throw new FExceptionInvalidOperation(`Text messages are not supported by the sub-protocol: ${subProtocol}`);
	}
}
export namespace FWebSocketChannelFactoryEndpoint {
	export interface BinaryChannel extends FDisposable, FChannelPublisher<Uint8Array>, FChannelSubscriber<Uint8Array> { }
	export interface TextChannel extends FDisposable, FChannelPublisher<string>, FChannelSubscriber<string> { }
}

export function instanceofWebServer(server: any): server is FWebServer {
	if (server instanceof UnsecuredWebServer) { return true; }
	if (server instanceof SecuredWebServer) { return true; }

	if (
		process.env["NODE_ENV"] === "development" &&
		"name" in server &&
		"underlyingServer" in server &&
		"rootExpressApplication" in server &&
		"bindRequestHandler" in server &&
		"createWebSocketServer" in server &&
		"listen" in server
	) {
		// Look like the server is WebServer like. Allow it only in development
		return true;
	}

	return false;
}

export function createWebServer(serverOpts: FHostingSettings.WebServer): FWebServer {
	switch (serverOpts.type) {
		case "http":
			return new UnsecuredWebServer(serverOpts);
		case "https":
			return new SecuredWebServer(serverOpts);
		default: {
			const { type } = serverOpts;
			throw new Error(`Not supported server type '${type}'`);
		}
	}
}

export function createWebServers(
	serversOpts: ReadonlyArray<FHostingSettings.WebServer>
): ReadonlyArray<FWebServer> {
	return serversOpts.map(serverOpts => createWebServer(serverOpts));
}


// function parseCertificate(certificate: Buffer | string): [pki.Certificate, Buffer] {
// 	let cert: pki.Certificate;
// 	let data: Buffer;

// 	if (typeof certificate === "string") {
// 		data = fs.readFileSync(certificate);
// 		cert = pki.certificateFromPem(data.toString("ascii"));
// 	} else {
// 		data = certificate;
// 		cert = pki.certificateFromPem(certificate.toString("ascii"));
// 	}

// 	return [cert, data];
// }
// function parseCertificates(certificates: Buffer | string | Array<string | Buffer>): Array<[pki.Certificate, Buffer]> {
// 	if (certificates instanceof Buffer || typeof certificates === "string") {
// 		return [parseCertificate(certificates)];
// 	} else {
// 		return certificates.map(parseCertificate);
// 	}
// }

namespace _FWebSocketChannelSupplyEndpointHelpers {
	export class WebSocketChannelBase<TData extends Parameters<WebSocket.WebSocket["send"]>[0]> {
		protected readonly _webSocket: WebSocket;
		protected readonly _callbacks: Array<FChannelSubscriber.Callback<TData>>;
		protected _isBroken: boolean;

		public constructor(webSocket: WebSocket) {
			this._webSocket = webSocket;
			this._callbacks = [];
			this._isBroken = false;
		}

		public async onClose(ex: FException): Promise<void> {
			if (this._isBroken) {
				// Already sent error-based callback, nothing to do
				return;
			}
			await Promise.all(this._callbacks.map(async (callback) => {
				try {
					// Notify that channel broken
					await callback(FExecutionContext.Empty, ex);
				} catch (e) {
					// Nothing to do any more with fucking client's callback. Just log STDERR.
					console.error(e);
				}
			}));
		}

		public async onMessage(_cancellationToken: FCancellationToken, data: TData): Promise<void> {
			if (this._isBroken) {
				console.error("Skip received messages due channel is broken");
				return;
			}
			const errors: Array<FException> = [];
			const safePromises = this._callbacks.map(async (callback) => {
				try {
					await callback(FExecutionContext.Empty, { data/*, FCancellationToken*/ });
				} catch (e) {
					errors.push(FException.wrapIfNeeded(e));
				}
			});
			await Promise.all(safePromises);
			if (errors.length > 0) {
				// The callback supplier is shit coder. Closing channel and socket to prevent flowing shit...
				this._isBroken = true;

				// https://tools.ietf.org/html/rfc6455#section-7.4.1
				this._webSocket.close(1011, "A server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request.");
				this._webSocket.terminate();

				const aggregatedError = new FExceptionAggregate(errors);
				await Promise.all(this._callbacks.map(async (callback) => {
					try {
						// Notify that channel broken
						await callback(FExecutionContext.Empty, aggregatedError);
					} catch (e) {
						// Nothing to do any more with fucking client's callback. Just log STDERR.
						console.error(e);
					}
				}));
			}
		}

		public addHandler(cb: FChannelSubscriber.Callback<TData>): void {
			this._callbacks.push(cb);
		}

		public removeHandler(cb: FChannelSubscriber.Callback<TData>): void {
			const index = this._callbacks.indexOf(cb);
			if (index !== -1) {
				this._callbacks.splice(index, 1);
			}
		}

		public send(_executionContext: FExecutionContext, data: TData): Promise<void> {
			if (this._isBroken) {
				throw new FExceptionInvalidOperation("Cannot send message on broken channel");
			}
			return new Promise<void>(sendResolve => this._webSocket.send(data, () => sendResolve()));
		}
	}
	export class WebSocketBinaryChannelImpl extends WebSocketChannelBase<Uint8Array>
		implements FWebSocketChannelSupplyEndpoint.BinaryChannel { }
	export class WebSocketTextChannelImpl extends WebSocketChannelBase<string>
		implements FWebSocketChannelSupplyEndpoint.TextChannel { }
}
