//
// Created by imper on 6/21/21.
//

#ifndef INET_COMM_INET_COMM_
#define INET_COMM_INET_COMM_

#include <cassert>
#include <thread>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <csignal>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/fcntl.h>
#include <log-console>
#include <ifaddrs.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

#define PRINT_PREFIX log_console::l_lock << log_console::l_localtime << l_location
#define DEBUG_COLOR color::faint << color::yellow

#define LOG (__detail__::_log_ << PRINT_PREFIX)
#define ERR (__detail__::_err_ << PRINT_PREFIX)
#define ENDENTLN color::reset << log_console::l_endent

#define ERROR(message) (ERR << color::red << color::bold << "error" << color::reset << " : " << color::red << color::italic << message << color::reset << color::red << "." << ENDENTLN)

#define DEFAULT_ERROR_HANDLER [](const char* str, size_t size, void* err_stream) { *static_cast<log_console*>(err_stream) << PRINT_PREFIX << color::red << str << ENDENTLN; return 0; }

#include <cstring>
#include <iostream>
#include <map>
#include <list>
#include <utility>
#include <vector>
#include <memory>

namespace inet
{
	class input_stream
	{
	public:
		template <typename... Args>
		static inline std::unique_ptr<input_stream> mkstream(Args&& ... args)
		{
			return std::move(std::make_unique<input_stream>(std::move(args)...));
		}
		
		inline input_stream() noexcept: bio(nullptr)
		{ }
		
		inline input_stream(BIO* bio) noexcept: bio(bio)
		{ }
		
		inline input_stream(const std::string& file) : bio(BIO_new(BIO_s_file()))
		{ BIO_read_filename(bio, file.c_str()); }
		
		inline input_stream(const std::vector<uint8_t>& key) : bio(nullptr)
		{
			int pipe[2];
			::pipe(pipe);
			::write(pipe[1], key.data(), key.size());
			::close(pipe[1]);
			bio = BIO_new_fd(pipe[0], BIO_CLOSE);
		}
		
		inline input_stream(int fd) : bio(BIO_new_fd(fd, BIO_CLOSE))
		{ }
		
		input_stream(const input_stream&) = delete;
		
		inline input_stream(input_stream&& stream) noexcept: bio(stream.bio)
		{ stream.bio = nullptr; }
		
		inline operator BIO*() const
		{ return bio; }
		
		inline ~input_stream()
		{ BIO_free_all(bio); }
	
	private:
		BIO* bio;
	};
	
	class loader
	{
	public:
		inline loader() = default;
		
		loader(const loader&) = delete;
		
		loader& operator=(const loader&) = delete;
		
		inline loader(loader&& another) = default;
		
		inline loader(std::unique_ptr<input_stream> cert, std::unique_ptr<input_stream> pkey) : cert(std::move(cert)), pkey(std::move(pkey))
		{ }
		
		inline X509* load_cert()
		{ return PEM_read_bio_X509(*cert, nullptr, nullptr, nullptr); }
		
		inline EVP_PKEY* load_pkey()
		{ return PEM_read_bio_PrivateKey(*pkey, nullptr, nullptr, nullptr); }
		
		inline operator bool() const
		{ return cert && pkey; }
		
		inline ~loader() = default;
	
	private:
		std::unique_ptr<input_stream> cert = nullptr;
		std::unique_ptr<input_stream> pkey = nullptr;
	};
	
	
	namespace __detail__ __attribute__((visibility("hidden")))
	{
		static log_console _log_(stdout);
		static log_console _err_(stderr);
		
		inline static SSL_CTX* init_server_ctx(const SSL_METHOD* method = DTLS_server_method())
		{
			SSL_CTX* ctx;
			
			OpenSSL_add_all_algorithms();  /* load & register all cryptos, etc. */
			SSL_load_error_strings();   /* load all error messages */
			
			ctx = SSL_CTX_new(method);   /* create new context from method */
			if (ctx == nullptr)
			{
				ERR_print_errors_cb(DEFAULT_ERROR_HANDLER, new log_console(stderr));
				return ctx;
			}
			
			SSL_CTX_set_cipher_list(ctx, "ALL:eNULL");
			
			return ctx;
		}
		
		inline static SSL_CTX* init_client_ctx(const SSL_METHOD* method = DTLS_client_method())
		{
			SSL_CTX* ctx;
			
			OpenSSL_add_all_algorithms();  /* load & register all cryptos, etc. */
			SSL_load_error_strings();   /* load all error messages */
			
			ctx = SSL_CTX_new(method);   /* create new context from method */
			if (ctx == nullptr)
			{
				ERR_print_errors_cb(DEFAULT_ERROR_HANDLER, new log_console(stderr));
				return ctx;
			}
			
			SSL_CTX_set_cipher_list(ctx, "ALL:eNULL");
			
			return ctx;
		}
		
		inline static int verify_callback(int, X509_STORE_CTX*)
		{
			return 1;
		}
		
		inline static bool load_certificates_server(SSL_CTX* ctx, X509* cert, EVP_PKEY* pkey)
		{
			if (SSL_CTX_set_default_verify_paths(ctx) != 1)
				ERR_print_errors_cb(DEFAULT_ERROR_HANDLER, new log_console(stderr));
			
			/** Set the local certificate from CertFile **/
			if (SSL_CTX_use_certificate(ctx, cert) <= 0)
			{
				ERR_print_errors_cb(DEFAULT_ERROR_HANDLER, new log_console(stderr));
				return false;
			}
			
			/** Set the private key from KeyFile (may be the same as CertFile) **/
			SSL_CTX_set_default_passwd_cb_userdata(ctx, (void*)"14880");
			if (SSL_CTX_use_PrivateKey(ctx, pkey) <= 0)
			{
				ERR_print_errors_cb(DEFAULT_ERROR_HANDLER, new log_console(stderr));
				return false;
			}
			
			/** Verify private key **/
			if (!SSL_CTX_check_private_key(ctx))
			{
				*new log_console(stderr) << log_console::l_localtime << l_location << color::red
										 << "Private key does not match the public certificate" << ENDENTLN;
				return false;
			}
			
			/** Force the client-side have a certificate **/
			SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, verify_callback);
			SSL_CTX_set_verify_depth(ctx, 4);
			
			return true;
		}
		
		inline static bool load_certificates_client(SSL_CTX* ctx, X509* cert, EVP_PKEY* pkey)
		{
			if (SSL_CTX_set_default_verify_paths(ctx) != 1)
				ERR_print_errors_cb(DEFAULT_ERROR_HANDLER, new log_console(stderr));
			
			/** Set the local certificate from CertFile **/
			if (SSL_CTX_use_certificate(ctx, cert) <= 0)
			{
				ERR_print_errors_cb(DEFAULT_ERROR_HANDLER, new log_console(stderr));
				return false;
			}
			
			/** Set the private key from KeyFile (may be the same as CertFile) **/
			SSL_CTX_set_default_passwd_cb_userdata(ctx, (void*)"14880");
			if (SSL_CTX_use_PrivateKey(ctx, pkey) <= 0)
			{
				ERR_print_errors_cb(DEFAULT_ERROR_HANDLER, new log_console(stderr));
				return false;
			}
			
			/** Verify private key **/
			if (!SSL_CTX_check_private_key(ctx))
			{
				*new log_console(stderr) << log_console::l_localtime << l_location << color::red
										 << "Private key does not match the public certificate" << ENDENTLN;
				return false;
			}
			
			/** Force the client-side have a certificate **/
			SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_callback);
			SSL_CTX_set_verify_depth(ctx, 4);
			
			return true;
		}
		
		inline static void print_cert_info(SSL* ssl)
		{
			X509* cert;
			char* line;
			
			cert = SSL_get_peer_certificate(ssl); /* Get certificates (if available) */
			if (cert)
			{
				auto log = LOG << "Server certificates:\n";
				line = X509_NAME_oneline(X509_get_subject_name(cert), nullptr, 0);
				log << "Subject: " << color::blue << line;
				delete[] line;
				line = X509_NAME_oneline(X509_get_issuer_name(cert), nullptr, 0);
				log << "Issuer: " << color::blue << line << ENDENTLN;
				delete[] line;
				X509_free(cert);
			}
			else
			{
				ERR << "No certificates." << ENDENTLN;
			}
		}
		
		template <typename T>
		inline void delete_buffer(T* buf)
		{ delete[] buf; }
	}
	
	/// Generates ECDSA key.
	inline EVP_PKEY* generate_pkey(std::string& error, const char* ec_type_name = "secp521r1")
	{
		/* Allocate memory for the EVP_PKEY structure. */
		auto* pkey = EVP_PKEY_new();
		if (!pkey)
		{
			error = "Unable to create EVP_PKEY structure.";
			return nullptr;
		}
		
		/* Allocate memory for the EC_KEY structure. */
		auto* ec_key = EC_KEY_new_by_curve_name(OBJ_txt2nid(ec_type_name));
		if (!ec_key)
		{
			error = "Unable to create EC_KEY structure.";
			return nullptr;
		}
		
		/* For cert signing, we use  the OPENSSL_EC_NAMED_CURVE flag*/
		EC_KEY_set_asn1_flag(ec_key, OPENSSL_EC_NAMED_CURVE);
		
		if (!EC_KEY_generate_key(ec_key))
		{
			error = "Unable to generate ECDSA key.";
			EC_KEY_free(ec_key);
			return nullptr;
		}
		
		if (!EVP_PKEY_assign_EC_KEY(pkey, ec_key))
		{
			error = "Unable to generate ECDSA key.";
			EC_KEY_free(ec_key);
			EVP_PKEY_free(pkey);
			return nullptr;
		}
		
		return pkey;
	}

/// Generates a self-signed x509 certificate.
	inline X509* generate_cert(
			std::string& error, EVP_PKEY* pkey, const std::string& country, const std::string& organization, const std::string& certificate_name,
			long serial_number = 1L, long valid_from = 0L, long valid_to = 31536000L)
	{
		/* Allocate memory for the X509 structure. */
		auto* cert = X509_new();
		if (!cert)
		{
			error = "Unable to create X509 structure.";
			return nullptr;
		}
		
		/* Set the serial number. */
		ASN1_INTEGER_set(X509_get_serialNumber(cert), serial_number);
		
		/* This certificate is valid from <valid_from seconds> until exactly <valid_to seconds>. Default from now to one year. */
		X509_gmtime_adj(X509_get_notBefore(cert), valid_from);
		X509_gmtime_adj(X509_get_notAfter(cert), valid_to);
		
		/* Set the public key for our certificate. */
		X509_set_pubkey(cert, pkey);
		
		/* We want to copy the subject name to the issuer name. */
		auto* name = X509_get_subject_name(cert);
		
		/* Set the country code and common name. */
		X509_NAME_add_entry_by_txt(
				name, "C", MBSTRING_ASC,
				(unsigned char*)country.data(), country.size(), 1, 0
		);
		X509_NAME_add_entry_by_txt(
				name, "O", MBSTRING_ASC,
				(unsigned char*)organization.data(), organization.size(), 2, 0
		);
		X509_NAME_add_entry_by_txt(
				name, "CN", MBSTRING_ASC,
				(unsigned char*)certificate_name.data(), certificate_name.size(), 3, 0
		);
		
		/* Now set the issuer name. */
		X509_set_issuer_name(cert, name);
		
		/* Actually sign the certificate with our key. */
		if (!X509_sign(cert, pkey, EVP_sha256()))
		{
			error = "Error signing certificate.";
			X509_NAME_free(name);
			X509_free(cert);
			return nullptr;
		}
		
		return cert;
	}
	
	inline auto convert(EVP_PKEY* pkey)
	{
		char* buf = nullptr;
		size_t size;
		auto stream = open_memstream(&buf, &size);
		PEM_write_PrivateKey(stream, pkey, nullptr, nullptr, 0, nullptr, nullptr);
		::fclose(stream);
		std::unique_ptr<char, decltype(&__detail__::delete_buffer<char>)> deleter(
				buf, &__detail__::delete_buffer<char>
		);
		return std::move(std::vector<uint8_t>(buf, buf + size));
	}
	
	inline auto convert(X509* cert)
	{
		char* buf = nullptr;
		size_t size;
		auto stream = open_memstream(&buf, &size);
		PEM_write_X509(stream, cert);
		::fclose(stream);
		std::unique_ptr<char, decltype(&__detail__::delete_buffer<char>)> deleter(
				buf, &__detail__::delete_buffer<char>
		);
		return std::move(std::vector<uint8_t>(buf, buf + size));
	}
	
	inline bool write_certificate_to_disk(
			std::string& error, EVP_PKEY* pkey, X509* cert,
			const char* key_filename = "certificate.key", const char* x509_filename = "certificate.pem")
	{
		/* Open the PEM file for writing the key to disk. */
		FILE* pkey_file = ::fopen(key_filename, "wb");
		if (!pkey_file)
		{
			error = std::string("Unable to open file \"") + key_filename + "\" for writing.";
			return false;
		}
		
		/* Write the key to disk. */
		bool ret = ::PEM_write_PrivateKey(pkey_file, pkey, nullptr, nullptr, 0, nullptr, nullptr);
		::fclose(pkey_file);
		
		if (!ret)
		{
			error = "Unable to write private key to disk.";
			return false;
		}
		
		/* Open the PEM file for writing the certificate to disk. */
		FILE* x509_file = ::fopen(x509_filename, "wb");
		if (!x509_file)
		{
			error = std::string("Unable to open \"") + x509_filename + "\" for writing.";
			return false;
		}
		
		/* Write the certificate to disk. */
		ret = ::PEM_write_X509(x509_file, cert);
		::fclose(x509_file);
		
		if (!ret)
		{
			error = "Unable to write certificate to disk.";
			return false;
		}
		
		return true;
	}
	
	inline void cleanup_certificate(EVP_PKEY* key, X509* x509)
	{
		EVP_PKEY_free(key);
		X509_free(x509);
	}
	
	inline static void open_port_in_iptables(uint16_t port)
	{
		if (!geteuid())
		{
			LOG << color::faint << DEBUG_COLOR << "executing opening ports command..." << ENDENTLN;
			std::string cmd("sudo iptables -A ");
			system((cmd + "INPUT -p tcp --dport " + std::to_string(port) + " -j ACCEPT").c_str());
			system((cmd + "OUTPUT -p tcp --dport " + std::to_string(port) + " -j ACCEPT").c_str());
			LOG << color::faint << DEBUG_COLOR << "executed." << ENDENTLN;
		}
	}
	
	inline static void close_port_in_iptables(uint16_t port)
	{
		if (!geteuid())
		{
			LOG << color::faint << DEBUG_COLOR << "executing opening ports command..." << ENDENTLN;
			std::string cmd("sudo iptables -D ");
			system((cmd + "INPUT -p tcp --dport " + std::to_string(port) + " -j ACCEPT").c_str());
			system((cmd + "OUTPUT -p tcp --dport " + std::to_string(port) + " -j ACCEPT").c_str());
			LOG << color::faint << DEBUG_COLOR << "executed." << ENDENTLN;
		}
	}
	
	typedef struct ifinfo
	{
		struct sockaddr if_addr = {AF_INET};
		struct sockaddr if_broadaddr = {AF_INET};
		struct sockaddr if_dstaddr = {AF_INET};
		struct sockaddr if_hwaddr = {0};
		struct sockaddr if_netmask = {AF_INET};
		struct ifmap if_map = { };
		char if_name[IFNAMSIZ]{ };
		short int if_flags = 0;
		int if_index = -1;
		int if_mtu = -1;
	} ifinfo, * pifinfo;
	
	inline static auto get_interface_information(const char* interface_name)
	{
		ifinfo result{ };
		int fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
		
		/* define the ifr_name - port name where network attached */
		::memcpy(result.if_name, interface_name, IFNAMSIZ - 1);
		
		/* address of the device using */
		struct ifreq tmp{ };
		::memcpy(tmp.ifr_name, interface_name, IFNAMSIZ - 1);
		::ioctl(fd, SIOCGIFADDR, &tmp);
		::memcpy(result.if_addr.sa_data, tmp.ifr_addr.sa_data, 14);
		
		/* broadcast address for a device */
		tmp = { };
		::memcpy(tmp.ifr_name, interface_name, IFNAMSIZ - 1);
		::ioctl(fd, SIOCGIFBRDADDR, &tmp);
		::memcpy(result.if_broadaddr.sa_data, tmp.ifr_broadaddr.sa_data, 14);
		
		/* destination address of a point-to-point device */
		tmp = { };
		::memcpy(tmp.ifr_name, interface_name, IFNAMSIZ - 1);
		::ioctl(fd, SIOCGIFDSTADDR, &tmp);
		::memcpy(result.if_dstaddr.sa_data, tmp.ifr_dstaddr.sa_data, 14);
		
		/* active flag word of the device */
		tmp = { };
		::memcpy(tmp.ifr_name, interface_name, IFNAMSIZ - 1);
		::ioctl(fd, SIOCGIFFLAGS, &tmp);
		result.if_flags = tmp.ifr_flags;
		
		/* hardware (MAC) address */
		tmp = { };
		::memcpy(tmp.ifr_name, interface_name, IFNAMSIZ - 1);
		::ioctl(fd, SIOCGIFHWADDR, &tmp);
		::memcpy(result.if_hwaddr.sa_data, tmp.ifr_hwaddr.sa_data, 14);
		
		/* index of the interface */
		tmp = { };
		::memcpy(tmp.ifr_name, interface_name, IFNAMSIZ - 1);
		::ioctl(fd, SIOCGIFINDEX, &tmp);
		result.if_index = tmp.ifr_ifindex;
		
		/* interface's hardware parameters */
		tmp = { };
		::memcpy(tmp.ifr_name, interface_name, IFNAMSIZ - 1);
		::ioctl(fd, SIOCGIFMAP, &tmp);
		result.if_map = tmp.ifr_map;
		
		/* MTU (Maximum Transfer Unit) of a device */
		tmp = { };
		::memcpy(tmp.ifr_name, interface_name, IFNAMSIZ - 1);
		::ioctl(fd, SIOCGIFMTU, &tmp);
		result.if_mtu = tmp.ifr_mtu;
		
		/* network mask for a device */
		tmp = { };
		::memcpy(tmp.ifr_name, interface_name, IFNAMSIZ - 1);
		::ioctl(fd, SIOCGIFNETMASK, &tmp);
		::memcpy(result.if_netmask.sa_data, tmp.ifr_netmask.sa_data, 14);
		
		::close(fd);
		return result;
	}
	
	inline static auto list_all_network_interfaces()
	{
		std::list<struct ifaddrs*> result;
		
		struct ifaddrs* ifaddr, * ifa;
		
		if (::getifaddrs(&ifaddr) == -1)
		{
			ERROR("getifaddrs() failed " << ::strerrorname_np(errno) << " " << color::bold << ::strerrordesc_np(errno));
			return result;
		}
		
		for (ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next)
		{
			if (ifa->ifa_addr == nullptr || ifa->ifa_addr->sa_family != AF_PACKET) continue;
			result.push_back(ifa);
		}
		
		return result;
	}
	
	inline in_addr aton(const char* address)
	{
		in_addr ret{0};
		::inet_aton(address, &ret);
		return ret;
	}
	
	class suppress_signal
	{
	public:
		inline explicit suppress_signal(int _sig) : sig(_sig)
		{ ::signal(_sig, sig_handle); }
		
		inline ~suppress_signal()
		{ ::signal(this->sig, SIG_DFL); }
		
		inline static void enable_log(bool status = true)
		{ if (status) _log_ << log_console::on; else _log_ << log_console::off; }
		
		inline static void enable_err(bool status = true)
		{ if (status) _err_ << log_console::on; else _err_ << log_console::off; }
	
	private:
		int sig;
		
		inline static void sig_handle(int sig)
		{ ERROR(::sigabbrev_np(sig) << " " << color::bold << ::sigdescr_np(sig)); }
		
		static log_console _log_;
		static log_console _err_;
	};

// # definition-begin suppress_signal
	
	log_console suppress_signal::_log_(stdout);
	log_console suppress_signal::_err_(stderr);

// # definition-end suppress_signal
	
	class server;
	
	class inet_address
	{
	public:
		inline inet_address() = default;
		
		inline inet_address(const sockaddr_in& address) : address(address)
		{ }
		
		inline inet_address(in_addr address, uint16_t port)
				: inet_address(sockaddr_in{AF_INET, ::htons(port), address})
		{ }
		
		inline inet_address(const std::string& address, uint16_t port)
				: inet_address(aton(address.c_str()), port)
		{ }
		
		inline static inet_address& from_ipv4(const std::string& address, uint16_t default_port)
		{
			auto& addr = *new inet_address;
			addr.address.sin_family = AF_INET;
			size_t pos = address.find(':');
			if (pos == std::string::npos)
			{
				addr.set_address(address.c_str());
				addr.set_port(default_port);
			}
			else
			{
				addr.set_address(address.substr(0, pos).c_str());
				if (++pos < address.size())
					addr.set_port(std::stoul(address.substr(pos, address.size() - pos)));
				else
					addr.set_port(default_port);
			}
			return addr;
		}
		
		[[nodiscard]] inline const char* get_address() const
		{ return ::inet_ntoa(this->address.sin_addr); }
		
		[[nodiscard]] inline uint16_t get_port() const
		{ return ::ntohs(this->address.sin_port); }
		
		[[nodiscard]] inline sockaddr_in get_raw() const
		{ return this->address; }
		
		inline void set_address(const char* address)
		{ ::inet_aton(address, &this->address.sin_addr); }
		
		inline void set_port(uint16_t port)
		{ this->address.sin_port = ::htons(port); }
		
		inline operator sockaddr_in() const
		{ return this->operator const sockaddr_in&(); }
		
		inline operator const sockaddr_in&() const
		{ return this->address; }
	
	private:
		friend class server;
		
		friend class inet_io;
		
		sockaddr_in address{AF_INET};
	};
	
	
	class inet_io;
	
	typedef bool (* client_process_function)(inet_io& io, const inet_address& address, server* server);
	
	class server
	{
	public:
		struct server_client_data
		{
			int socket;
			std::map<int, bool>* is_client_processing_ptr;
			SSL* ssl;
			inet_address address;
			server* serv;
			client_process_function process_function;
			int thread_id;
			int* threads_count;
		};
		
		inline server(
				int max_clients, int timeo, const inet_address& address, client_process_function process_function,
				void* extra, loader&& crt);
		
		inline virtual bool run(bool wait);
		
		inline virtual void run_server_thread(bool wait_for_child_threads);
		
		inline static void wait_for_all_servers();
		
		[[nodiscard]] inline int get_socket() const
		{ return socket; }
		
		[[nodiscard]] inline int get_max_clients() const
		{ return max_clients; }
		
		[[nodiscard]] inline int get_timeo() const
		{ return timeo; }
		
		inline void set_timeo(int delay);
		
		[[nodiscard]] inline virtual const inet_address& get_address() const
		{ return this->address; }
		
		[[nodiscard]] inline virtual client_process_function get_process_function() const
		{ return process_function; }
		
		inline ~server();
		
		void* extra;
	
	private:
		inline static void* server_thread(void* args);
		
		inline static void* process_client(void* args);
		
		inline void set_sockopts()
		{
			struct timeval timeout{timeo / 1000, (timeo % 1000) * 1000};
			
			if (setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof timeout) < 0)
				ERROR("setsockopt() failed " << ::strerrorname_np(errno) << " "
											 << color::bold << ::strerrordesc_np(errno) << color::reset
											 << color::red << ". socket : " << socket << ". this = " << this);
			
			if (setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof timeout) < 0)
				ERROR("setsockopt() failed " << ::strerrorname_np(errno) << " "
											 << color::bold << ::strerrordesc_np(errno) << color::reset
											 << color::red << ". socket : " << socket << ". this = " << this);
		}
		
		pthread_t main_thread = 0;
		int socket;
		SSL_CTX* ctx = nullptr;
		bool wait_for_child_threads = false;
		
		int max_clients, timeo;
		inet_address address;
		client_process_function process_function;
		static pthread_mutex_t client_process_mutex;
		static pthread_mutex_t clients_counter_mutex;
		static int server_threads;
		static pthread_mutex_t server_threads_counter_mutex;
	};
	
	
	class inet_io
	{
	public:
		inline inet_io() : socket(-1), ssl(nullptr)
		{ }
		
		inline inet_io(int socket) : socket(socket), ssl(nullptr)
		{ }
		
		inline inet_io(SSL* ssl) : socket(ssl ? SSL_get_fd(ssl) : -1), ssl(ssl)
		{ }
		
		inline virtual ssize_t write(const void* data, int size);
		
		inline virtual ssize_t read(void* data, size_t size);
		
		[[nodiscard]] inline int get_socket() const
		{ return socket; }
		
		[[nodiscard]] inline SSL* get_ssl() const
		{ return this->ssl; }
		
		inline virtual operator bool() const;
	
	protected:
		int socket;
		SSL* ssl;
		bool success = true;
	};
	
	
	class client : public inet_io
	{
	public:
		inline client(int timeo, const inet_address& server_address, loader&& crt);
		
		[[nodiscard]] inline uint16_t get_port() const
		{ return this->server_address.get_port(); }
		
		[[nodiscard]] inline const inet_address& get_server_address() const
		{ return server_address; }
		
		[[nodiscard]] inline SSL_CTX* get_ctx() const
		{ return this->ctx; }
		
		inline ~client();
	
	private:
		inet_address server_address;
		SSL_CTX* ctx;
	};


// #definition-begin server
	
	int server::server_threads = 0;
	pthread_mutex_t server::client_process_mutex = PTHREAD_MUTEX_INITIALIZER;
	pthread_mutex_t server::clients_counter_mutex = PTHREAD_MUTEX_INITIALIZER;
	pthread_mutex_t server::server_threads_counter_mutex = PTHREAD_MUTEX_INITIALIZER;
	
	
	inline server::server(
			int max_clients, int timeo, const inet_address& address,
			client_process_function process_function, void* extra = nullptr, loader&& crt = { })
			: max_clients(max_clients), timeo(timeo),
			  address(address), process_function(process_function), extra(extra)
	{
		LOG << DEBUG_COLOR << "socket()..." << color::reset << "\n"
			<< DEBUG_COLOR << "server address: " << color::blue_bg << color::black << address.get_address() << color::reset << DEBUG_COLOR << ";"
			<< color::reset << "\n"
			<< DEBUG_COLOR << "port = " << address.get_port() << DEBUG_COLOR << ";" << color::reset << "\n"
			<< DEBUG_COLOR << "\tmax connections accept = " << max_clients << DEBUG_COLOR << ";" << color::reset << "\n"
			<< DEBUG_COLOR << "this = " << this << DEBUG_COLOR << ";" << ENDENTLN;
		socket = ::socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (socket < 0)
		{
			ERROR("socket() failed " << ::strerrorname_np(errno) << " " << color::bold << ::strerrordesc_np(errno) << color::reset
									 << color::red << ". socket : " << socket << ". this = " << this);
			throw std::runtime_error("socket() failed");
		}
		
		LOG << DEBUG_COLOR << "bind()..." << color::reset << "\n" << DEBUG_COLOR << "this = " << this << DEBUG_COLOR << ";" << ENDENTLN;
		if (::bind(socket, (struct sockaddr*)&address.address, sizeof(address.address)) < 0)
		{
			ERROR("bind() failed " << ::strerrorname_np(errno) << " " << color::bold << ::strerrordesc_np(errno) << color::reset
								   << color::red << ". socket : " << socket << ". this = " << this);
			throw std::runtime_error("bind() failed");
		}
		
		LOG << DEBUG_COLOR << "listening socket = " << color::reset << socket << DEBUG_COLOR << " ..." << ENDENTLN;
		if (listen(socket, max_clients) < 0)
		{
			ERROR("listen() failed " << ::strerrorname_np(errno) << " " << color::bold << ::strerrordesc_np(errno) << color::reset
									 << color::red << ". socket : " << socket << ". this = " << this);
			throw std::runtime_error("listen() failed");
		}
		
		if (crt)
		{
			LOG << DEBUG_COLOR << "init_server_ctx()..." << ENDENTLN;
			ctx = __detail__::init_server_ctx();
			
			LOG << DEBUG_COLOR << "load_certificates()..." << ENDENTLN;
			if (!__detail__::load_certificates_server(ctx, crt.load_cert(), crt.load_pkey()))
			{
				ERROR("load_certificates() returned " << false << color::red << ". socket : " << socket << ". this = " << this);
			}
		}
		
		LOG << DEBUG_COLOR << "success." << ENDENTLN;
	}
	
	inline bool server::run(bool wait)
	{
		int threads_count = 0, thread_id = 0;
		
		std::map<int, bool> is_client_processing;
		
		struct sockaddr_in address{ };
		socklen_t addrlen = sizeof(address);
		
		int new_socket;
		
		while (!::fcntl(socket, F_GETFD))
		{
			struct timeval timeout{timeo / 1000, (timeo % 1000) * 1000};
			
			fd_set readfds;
			FD_ZERO(&readfds);
			FD_SET(socket, &readfds);
			
			if (::select(socket + 1, &readfds, nullptr, nullptr, &timeout) > 0 && !::fcntl(socket, F_GETFD))
			{
				if (new_socket = ::accept(socket, (struct sockaddr*)&address, &addrlen); new_socket < 0)
				{
					ERROR("accept() failed " << ::strerrorname_np(errno) << " "
											 << color::bold << ::strerrordesc_np(errno) << color::reset
											 << color::red << ". socket : " << socket << ". this = " << this);
					return errno == EBADF || errno == EINVAL;
				}
				
				LOG << DEBUG_COLOR << "+====== new connection ======/’" << color::reset << "\n"
					<< DEBUG_COLOR << "| socket = " << color::reset << new_socket << DEBUG_COLOR << ";" << color::reset << "\n"
					<< DEBUG_COLOR << "| address = " << color::blue_bg << color::black << ::inet_ntoa(address.sin_addr) << DEBUG_COLOR << ";"
					<< color::reset << "\n"
					<< DEBUG_COLOR << "| port = " << color::reset << ::ntohs(address.sin_port) << DEBUG_COLOR << ";" << color::reset << "\n\n"
					<< DEBUG_COLOR << "+============================\\," << ENDENTLN;
				
				SSL* ssl = nullptr;
				
				if (ctx)
				{
					ssl = SSL_new(ctx); /** raw new SSL state with context **/
					if (!ssl)
					{
						ERROR("SSL_new() returned " << color::italic << color::purple << "nullptr" << color::reset
													<< color::red << ". socket : " << socket << ". this = " << this);
						return true;
					}
					
					if (!SSL_set_fd(ssl, new_socket)) /** set connection socket to SSL state **/
					{
						ERR_print_errors_cb(DEFAULT_ERROR_HANDLER, &__detail__::_err_);
						return true;
					}
					
					if (ssl)
					{
						if (SSL_accept(ssl) <= 0)
						{
							ERROR("SSL_accept() failed. socket : " << socket << ". this = " << this);
							ERR_print_errors_cb(DEFAULT_ERROR_HANDLER, &__detail__::_err_);
							ssl = nullptr;
							return true;
						}
					}
					
					__detail__::print_cert_info(ssl);
				}
				
				auto* dat = new server_client_data{ };
				dat->socket = new_socket;
				dat->ssl = ssl;
				dat->serv = const_cast<server*>(this);
				dat->address = inet_address(address);
				dat->is_client_processing_ptr = &is_client_processing;
				dat->thread_id = ++thread_id;
				::pthread_mutex_lock(&clients_counter_mutex);
				dat->threads_count = &++threads_count;
				::pthread_mutex_unlock(&clients_counter_mutex);
				dat->process_function = process_function;
				
				LOG << DEBUG_COLOR << "running client processing thread..." << ENDENTLN;
				
				pthread_t pthread;
				::pthread_create(&pthread, nullptr, process_client, (void*)dat);
				::pthread_detach(pthread);
				
				LOG << DEBUG_COLOR << "done." << ENDENTLN;
			}
			else ::usleep(10000);
		}
		
		while (wait && threads_count > 0)
		{
			::usleep(10000);
		}
		
		return !::fcntl(socket, F_GETFD);
	}
	
	void server::set_timeo(int delay)
	{
		timeo = delay;
		set_sockopts();
	}
	
	inline void* server::server_thread(void* args)
	{
		auto* server = static_cast<class server*>(args);
		while (server->run(server->wait_for_child_threads));
		::pthread_mutex_lock(&server_threads_counter_mutex);
		--server::server_threads;
		::pthread_mutex_unlock(&server_threads_counter_mutex);
		return nullptr;
	}
	
	inline void server::run_server_thread(bool wait_for_child_threads = false)
	{
		this->wait_for_child_threads = wait_for_child_threads;
		::pthread_mutex_lock(&server_threads_counter_mutex);
		++server::server_threads;
		::pthread_mutex_unlock(&server_threads_counter_mutex);
		LOG << DEBUG_COLOR << "starting main server thread..." << ENDENTLN;
		::pthread_create(&this->main_thread, nullptr, server_thread, static_cast<void*>(const_cast<server*>(this)));
		::pthread_detach(this->main_thread);
		LOG << DEBUG_COLOR << "done." << ENDENTLN;
	}
	
	inline void server::wait_for_all_servers()
	{
		while (server::server_threads > 0)
			::usleep(10000);
	}
	
	inline void* server::process_client(void* args)
	{
		auto& data = *static_cast<struct server_client_data*>(args);
		int client_socket = data.socket;
		
		LOG << DEBUG_COLOR << "socket : " << client_socket << ENDENTLN;
		if ((*data.is_client_processing_ptr)[client_socket])
			return nullptr;
		
		::pthread_mutex_lock(&client_process_mutex);
		(*data.is_client_processing_ptr)[client_socket] = true;
		::pthread_mutex_unlock(&client_process_mutex);
		
		LOG << DEBUG_COLOR << "processing client..." << color::reset << "\n"
			<< DEBUG_COLOR << "\tthread_no = " << data.thread_id << DEBUG_COLOR << "; this = " << data.serv << color::reset << "\n"
			<< DEBUG_COLOR << "sock = " << data.socket << DEBUG_COLOR << "; ssl = " << data.ssl << ENDENTLN;
		auto io = (data.ssl ? inet_io(data.ssl) : inet_io(data.socket));
		while (data.process_function(io, data.address, data.serv));
		
		LOG << DEBUG_COLOR << "SSL_free()..." << ENDENTLN;
		SSL_free(data.ssl);
		
		LOG << color::faint << "job finished.\n\tthread_no = " << data.thread_id << ";" << ENDENTLN;
		
		::pthread_mutex_lock(&client_process_mutex);
		(*data.is_client_processing_ptr)[client_socket] = false;
		::pthread_mutex_unlock(&client_process_mutex);
		
		::pthread_mutex_lock(&clients_counter_mutex);
		--*data.threads_count;
		::pthread_mutex_unlock(&clients_counter_mutex);
		return nullptr;
	}
	
	inline server::~server()
	{
		LOG << DEBUG_COLOR << "destruction of server(); this = " << this << ENDENTLN;
		pthread_kill(this->main_thread, 0);
		::close(this->socket);
		this->socket = -1;
		SSL_CTX_free(this->ctx);
	}

// #definition-end server

// #definition-begin inet_io
	inline ssize_t inet_io::write(const void* data, int size)
	{
		ssize_t wrote;
		this->success = true;
		if (this->ssl)
		{
			if ((wrote = SSL_write(this->ssl, data, size) <= 0))
			{
				ERR << "this = " << this << ENDENTLN;
				ERR_print_errors_cb(DEFAULT_ERROR_HANDLER, &__detail__::_err_);
				this->success = false;
			}
		}
		else if ((wrote = ::send(this->socket, data, size, 0)) <= 0)
		{
			ERR << "this = " << this << ENDENTLN;
			ERROR("write() failed " << ::strerrorname_np(errno) << " " << color::bold << ::strerrordesc_np(errno));
			this->success = false;
		}
		return wrote;
	}
	
	inline ssize_t inet_io::read(void* data, size_t size)
	{
		this->success = true;
		
		ssize_t read;
		if (this->ssl)
			read = SSL_read(this->ssl, data, size);
		else
			read = ::recv(this->socket, data, size, 0);
		
		if (read <= 0)
		{
			ERR << "this = " << this << ENDENTLN;
			
			if (this->ssl)
				ERR_print_errors_cb(DEFAULT_ERROR_HANDLER, &__detail__::_err_);
			else
				ERROR("read() failed " << ::strerrorname_np(errno) << " " << color::bold << ::strerrordesc_np(errno));
			
			this->success = false;
		}
		
		return read;
	}
	
	inline inet_io::operator bool() const
	{ return this->success; }

// #definition-end inet_io

// #definition-begin client
	
	inline client::client(int timeo, const inet_address& server_address, loader&& crt = { })
			: server_address(server_address)
	{
		LOG << DEBUG_COLOR << "socket init..." << color::reset << "\n"
			<< DEBUG_COLOR << "server address: " << color::blue_bg << color::black << server_address.get_address() << DEBUG_COLOR << ";"
			<< color::reset << "\n"
			<< DEBUG_COLOR << "port = " << server_address.get_port() << DEBUG_COLOR << ";" << ENDENTLN;
		
		socket = ::socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (socket < 0)
		{
			ERROR("socket() failed " << ::strerrorname_np(errno) << " " << color::bold << ::strerrordesc_np(errno) << color::reset << color::red
									 << ". socket : " << socket << ". this = " << this);
			throw std::runtime_error("socket() failed");
		}
		
		ctx = nullptr;
		if (crt)
		{
			LOG << DEBUG_COLOR << "initializing ctx..." << ENDENTLN;
			ctx = __detail__::init_client_ctx();
			LOG << DEBUG_COLOR << "loading certificates..." << ENDENTLN;
			if (!__detail__::load_certificates_client(ctx, crt.load_cert(), crt.load_pkey()))
			{
				ERROR("load_certificates() returned " << false << color::red << ". this = " << this);
				SSL_CTX_free(ctx);
				ctx = nullptr;
			}
		}
		
		fd_set set{ };
		FD_ZERO(&set);
		FD_SET(socket, &set);
		
		struct timeval timeout{timeo / 1000, (timeo % 1000) * 1000};
		
		int opt;
		if ((opt = ::fcntl(socket, F_GETFL, nullptr)) < 0)
		{
			ERROR("fcntl() failed " << ::strerrorname_np(errno) << " " << color::bold << ::strerrordesc_np(errno));
			opt = 0;
		}
		
		if (::fcntl(socket, F_SETFL, opt | O_NONBLOCK) < 0)
		{
			ERROR("fcntl() failed " << ::strerrorname_np(errno) << " " << color::bold << ::strerrordesc_np(errno));
			throw std::runtime_error("fcntl() failed");
		}
		
		LOG << DEBUG_COLOR << "connecting..." << ENDENTLN;
		
		if (::connect(socket, (struct sockaddr*)&server_address, sizeof(server_address)) < 0)
		{
			if (errno != EINPROGRESS)
			{
				ERROR("connect() failed " << ::strerrorname_np(errno) << " " << color::bold << ::strerrordesc_np(errno));
				throw std::runtime_error("connect() failed");
			}
		}
		
		if (::select(socket + 1, nullptr, &set, nullptr, &timeout) <= 0)
		{
			ERROR("select() failed " << ::strerrorname_np(errno) << " " << color::bold << ::strerrordesc_np(errno));
			throw std::runtime_error("select() failed");
		}
		
		if ((opt = ::fcntl(socket, F_GETFL, nullptr)) < 0)
		{
			ERROR("fcntl() failed " << ::strerrorname_np(errno) << " " << color::bold << ::strerrordesc_np(errno));
			opt = 0;
		}
		
		if (::fcntl(socket, F_SETFL, opt & ~O_NONBLOCK) < 0)
		{
			ERROR("fcntl() failed " << ::strerrorname_np(errno) << " " << color::bold << ::strerrordesc_np(errno));
			throw std::runtime_error("fcntl() failed");
		}
		
		if (ctx)
		{
			if (!(ssl = SSL_new(ctx)))
			{
				ERROR("SSL_new() failed.");
				ERR_print_errors_cb(DEFAULT_ERROR_HANDLER, &__detail__::_err_);
				throw std::runtime_error("SSL_new() failed");
			}
			
			SSL_set_fd(ssl, this->socket);
			
			LOG << DEBUG_COLOR << "SSL_connect()..." << ENDENTLN;
			
			if (SSL_connect(ssl) <= 0)
			{
				ERR << "this = " << this << ENDENTLN;
				ERR_print_errors_cb(DEFAULT_ERROR_HANDLER, &__detail__::_err_);
				ssl = nullptr;
				throw std::runtime_error("SSL_connect() failed");
			}
			
			__detail__::print_cert_info(ssl);
		}
		
		LOG << DEBUG_COLOR << "success." << ENDENTLN;
	}
	
	inline client::~client()
	{
		LOG << DEBUG_COLOR << "destruction of client(); this = " << this << ENDENTLN;
		::close(this->socket);
		SSL_free(this->ssl);
	}

// #definition-end client
}

#endif // INET_COMM_INET_COMM_