#include "microhttpd_server.h"
#include "piliterals_bytes.h"
#include "piliterals_string.h"
#include "piliterals_time.h"

#include <microhttpd.h>
// clang-format off
#ifdef QNX
#  include <arpa/inet.h>
#  include <sys/socket.h>
#  include <sys/types.h>
#  ifdef BLACKBERRY
#	include <netinet/in.h>
#  else
#	include <sys/dcmd_io-net.h>
#  endif
#else
#  ifdef WINDOWS
#    include <io.h>
#    include <winsock2.h>
#    include <ws2tcpip.h>
#  else
#    include <netinet/in.h>
#    include <sys/socket.h>
#    ifdef LWIP
#      include <lwip/sockets.h>
#    endif
#  endif
#endif
// clang-format on


using namespace PIHTTP;


struct BasicAuthCred {
	bool exists = false;
	PIString user;
	PIString pass;
};

struct MicrohttpdServerConnection {
	bool checkBasicAuth();
	bool ready();
	int sendReply(const MessageMutable & r);
	BasicAuthCred getBasicAuthCred();

	bool done = false, authorized = false;
	Method method = Method::Unknown;
	PIString path;
	PIByteArray body;
	PIMap<PIString, PIString> headers, args, post;
	MHD_Connection * connection       = nullptr;
	MicrohttpdServer * server         = nullptr;
	MHD_PostProcessor * postprocessor = nullptr;
};


bool MicrohttpdServerConnection::checkBasicAuth() {
	if (headers.contains(Header::Authorization)) {
		auto ba_up = getBasicAuthCred();
		bool ok    = false; // server->callback_auth(ba_up.user, ba_up.pass);
		if (server->callback_auth) ok = server->callback_auth(ba_up.user, ba_up.pass);
		if (ok) {
			authorized = true;
			return true;
		}
	}
	// piCout << "miss authorization";
	sendReply(MessageMutable::fromCode(Code::Unauthorized)
				  .addHeader(Header::WWWAuthenticate, "Basic realm=\"%1\", charset=\"UTF-8\""_a.arg(server->realm))
				  .setBody(PIByteArray::fromAscii("Authorization required")));
	// piCout << "answer sent";
	return false;
}


bool MicrohttpdServerConnection::ready() {
	if (!server) return false;
	if (done) return true;
	done = true;
	MessageMutable rep;
	if (method == Method::Get) {
		if (path == "/favicon.ico"_a) {
			// piCout << "send favicon" << server->favicon.size() << "bytes";
			rep.setBody(server->favicon);
			sendReply(rep);
			return true;
		}
	}
	// piCout << "ready" << (int)method << path << body.size();
	MessageMutable req;
	req.setMethod(method);
	req.setPath(path);
	req.setBody(body);
	req.headers()        = headers;
	req.queryArguments() = args;
	rep.setCode(Code::BadRequest);
	if (server->callback) rep = server->callback(req);
	MicrohttpdServer::addFixedHeaders(rep);
	sendReply(rep);
	// piCout << "ready ok" << (int)rep.code() << rep.body().size();
	return true;
}


int MicrohttpdServerConnection::sendReply(const MessageMutable & r) {
	MHD_Response * response = MHD_create_response_from_buffer(r.body().size(), (void *)r.body().data(), MHD_RESPMEM_MUST_COPY);
	if (!response) {
		// piCout << "null response" << r.body.size() << (void *)r.body.data();
		return MHD_NO;
	}
	auto it = r.headers().makeIterator();
	while (it.next())
		MHD_add_response_header(response, it.key().dataAscii(), it.value().dataUTF8());
	// piCout << "status" << r.code;
	int ret = MHD_queue_response(connection, static_cast<int>(r.code()), response);
	MHD_destroy_response(response);
	return ret;
}


BasicAuthCred MicrohttpdServerConnection::getBasicAuthCred() {
	BasicAuthCred ret;
	char * p = nullptr;
	auto u   = MHD_basic_auth_get_username_password(connection, &p);
	if (u) {
		ret.user   = PIString::fromUTF8(u);
		ret.exists = true;
		MHD_free(u);
	}
	if (p) {
		ret.pass   = PIString::fromUTF8(p);
		ret.exists = true;
		MHD_free(p);
	}
	return ret;
}


void log_callback(void * cls, const char * fmt, va_list ap) {
	MicrohttpdServer * server = (MicrohttpdServer *)cls;
	piCout << "log" << server;
	if (!server) return;
	char buffer[1_KiB];
	piZeroMemory(buffer, 1_KiB);
	std::vsnprintf(buffer, 1_KiB, fmt, ap);
	piCout << buffer;
}


int iterate_post(void * conn_cls,
                 MHD_ValueKind kind,
                 const char * key,
                 const char * filename,
                 const char * content_type,
                 const char * transfer_encoding,
                 const char * data,
                 uint64_t off,
                 size_t size) {
	MicrohttpdServerConnection * conn = (MicrohttpdServerConnection *)conn_cls;
	if (!conn) return MHD_NO;
	conn->post[PIString::fromUTF8(key)] = PIString::fromUTF8(data);
	return MHD_YES;
}


void request_completed(void * cls, MHD_Connection * connection, void ** con_cls, MHD_RequestTerminationCode toe) {
	MicrohttpdServerConnection *& conn((MicrohttpdServerConnection *&)(*con_cls));
	// piCout << "request_completed" << conn << conn->headers << conn->post << '"' << conn->body << '"';
	if (!conn) return;
	if (conn->postprocessor) {
		MHD_destroy_post_processor(conn->postprocessor);
		conn->postprocessor = nullptr;
	}
	conn->ready();
	piDeleteSafety(conn);
}


int header_iterate(void * cls, MHD_ValueKind kind, const char * key, const char * value) {
	MicrohttpdServerConnection * conn = (MicrohttpdServerConnection *)cls;
	if (!conn) return MHD_NO;
	conn->headers[PIString::fromUTF8(key)] = PIString::fromUTF8(value);
	return MHD_YES;
}


int args_iterate(void * cls, MHD_ValueKind kind, const char * key, const char * value) {
	MicrohttpdServerConnection * conn = (MicrohttpdServerConnection *)cls;
	if (!conn) return MHD_NO;
	conn->args[PIString::fromUTF8(key)] = PIString::fromUTF8(value);
	return MHD_YES;
}


int answer_callback(void * cls,
                    MHD_Connection * connection,
                    const char * url,
                    const char * method,
                    const char * version,
                    const char * upload_data,
                    size_t * upload_data_size,
                    void ** con_cls) {
	MicrohttpdServer * server = (MicrohttpdServer *)cls;

	Method m                  = Method::Unknown;

	if (0 == strcmp(method, "GET"))
		m = Method::Get;
	else if (0 == strcmp(method, "POST"))
		m = Method::Post;
	else if (0 == strcmp(method, "HEAD"))
		m = Method::Head;
	else if (0 == strcmp(method, "PUT"))
		m = Method::Put;
	else if (0 == strcmp(method, "DELETE"))
		m = Method::Delete;
	else if (0 == strcmp(method, "CONNECT"))
		m = Method::Connect;
	else if (0 == strcmp(method, "OPTIONS"))
		m = Method::Options;
	else if (0 == strcmp(method, "TRACE"))
		m = Method::Trace;
	else if (0 == strcmp(method, "PATCH"))
		m = Method::Patch;

	if (m == Method::Unknown) {
		piCout << "[MicrohttpdServer]"
			   << "Warning:"
			   << "Unknown method!";
		return MHD_NO;
	}

	// piCout << "answer" << url << method << (int)m << server;
	MicrohttpdServerConnection *& conn((MicrohttpdServerConnection *&)(*con_cls));
	if (!conn) {
		conn             = new MicrohttpdServerConnection();
		conn->connection = connection;
		conn->server     = server;
		conn->path       = PIString::fromUTF8(url);
		conn->method     = m;
		MHD_get_connection_values(connection, MHD_HEADER_KIND, (MHD_KeyValueIterator)header_iterate, *con_cls);
		MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, (MHD_KeyValueIterator)args_iterate, *con_cls);
		if (server->isBasicAuthEnabled() && !conn->authorized) {
			if (!conn->checkBasicAuth()) return MHD_YES;
		}
		return MHD_YES;
	}

	if (m == Method::Unknown) {
		return conn->sendReply(MessageMutable::fromCode(Code::MethodNotAllowed));
	}


	if (*upload_data_size) {
		if (!conn->postprocessor) {
			conn->postprocessor = MHD_create_post_processor(connection, 64_KiB, (MHD_PostDataIterator)iterate_post, (void *)conn);
		}
		conn->body.append(upload_data, *upload_data_size);
		MHD_post_process(conn->postprocessor, upload_data, *upload_data_size);
		*upload_data_size = 0;
	} else {
		// qDebug() << "answer ok";
		if (!conn->ready()) return conn->sendReply(MessageMutable::fromCode(Code::InternalServerError));
	}
	return MHD_YES;
}


PRIVATE_DEFINITION_START(MicrohttpdServer)
	MHD_Daemon * daemon;
PRIVATE_DEFINITION_END(MicrohttpdServer)


MicrohttpdServer::MicrohttpdServer() {
	PRIVATE->daemon                 = nullptr;
	opts[Option::ConnectionLimit]   = FD_SETSIZE - 4;
	opts[Option::ConnectionTimeout] = 0_s;
	realm                           = "Restricted"_a;
}


MicrohttpdServer::~MicrohttpdServer() {
	stop();
}


void MicrohttpdServer::setOption(Option o, PIVariant v) {
	opts[o] = std::move(v);
}


void MicrohttpdServer::setFavicon(const PIByteArray & im) {
	favicon = im;
}


bool MicrohttpdServer::listen(PINetworkAddress addr) {
	stop();
	uint flags = 0;
#if MHD_VERSION <= 0x00095100
	flags |= MHD_USE_POLL_INTERNALLY;
#else
	flags |= MHD_USE_AUTO | MHD_USE_INTERNAL_POLLING_THREAD;
#endif
	if (opts.value(Option::HTTPSEnabled).toBool()) flags |= MHD_USE_TLS;
	mem_key = opts.value(Option::HTTPSMemKey).toByteArray();
	if (mem_key.isNotEmpty()) mem_key.append(0);
	mem_cert = opts.value(Option::HTTPSMemCert).toByteArray();
	if (mem_cert.isNotEmpty()) mem_cert.append(0);
	key_pass = opts.value(Option::HTTPSKeyPassword).toByteArray();
	if (key_pass.isNotEmpty()) key_pass.append(0);
	sockaddr_in sa_addr;
	piZeroMemory(sa_addr);
	sa_addr.sin_port        = htons(addr.port());
	sa_addr.sin_addr.s_addr = addr.ip();
	sa_addr.sin_family      = AF_INET;
	PIVector<MHD_OptionItem> options;
	options.append({MHD_OPTION_EXTERNAL_LOGGER, (intptr_t)log_callback, this});
	options.append({MHD_OPTION_NOTIFY_COMPLETED, (intptr_t)request_completed, nullptr});
	options.append({MHD_OPTION_CONNECTION_LIMIT, opts.value(Option::ConnectionLimit).toInt(), nullptr});
	options.append({MHD_OPTION_CONNECTION_TIMEOUT, piRound(opts.value(Option::ConnectionTimeout).toSystemTime().toSeconds()), nullptr});
	options.append({MHD_OPTION_SOCK_ADDR, 0, &sa_addr});
	if (opts.value(Option::HTTPSEnabled).toBool()) {
		options.append({MHD_OPTION_HTTPS_MEM_KEY, 0, mem_key.data()});
		options.append({MHD_OPTION_HTTPS_MEM_CERT, 0, mem_cert.data()});
		options.append({MHD_OPTION_HTTPS_KEY_PASSWORD, 0, key_pass.data()});
	}
	options.append({MHD_OPTION_END, 0, nullptr});
	PRIVATE->daemon = MHD_start_daemon(flags,
									   addr.port(),
									   nullptr,
									   nullptr,
									   (MHD_AccessHandlerCallback)answer_callback,
									   this,
									   MHD_OPTION_ARRAY,
									   options.data(),
									   MHD_OPTION_END);
	return isListen();
}


bool MicrohttpdServer::isListen() const {
	return PRIVATE->daemon;
}


void MicrohttpdServer::stop() {
	if (PRIVATE->daemon) {
		MHD_stop_daemon(PRIVATE->daemon);
		PRIVATE->daemon = nullptr;
	}
}


void MicrohttpdServer::addFixedHeaders(MessageMutable & msg) {
	if (!msg.headers().contains(Header::ContentType)) {
		if (msg.body().isNotEmpty()) {
			if (msg.body().startsWith(PIByteArray::fromAscii("<!DOCTYPE html>")))
				msg.addHeader(Header::ContentType, "text/html; charset=utf-8");
			else if (msg.body()[0] == '[' || msg.body()[0] == '{')
				msg.addHeader(Header::ContentType, "application/json; charset=utf-8");
		}
	}
	// msg.addHeader(Header::AccessControlAllowOrigin, "*");
}
