#include <mlibc/lookup.hpp>
#include <mlibc/resolv_conf.hpp>
#include <mlibc/debug.hpp>
#include <mlibc/services.hpp>
#include <bits/ensure.h>

#include <frg/string.hpp>
#include <mlibc/allocator.hpp>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <ctype.h>

namespace mlibc {

namespace {
	constexpr unsigned short RETURN_NOERROR [[maybe_unused]] = 0x0;
	constexpr unsigned short RETURN_NXDOMAIN = 0x3;

	constexpr unsigned int RECORD_A = 1;
	constexpr unsigned int RECORD_CNAME = 5;
	constexpr unsigned int RECORD_PTR = 12;
	constexpr unsigned int RECORD_AAAA = 28;
} // namespace

static frg::string<MemoryAllocator> read_dns_name(char *buf, char *&it) {
	frg::string<MemoryAllocator> res{getAllocator()};
	while (true) {
		char code = *it++;
		if ((code & 0xC0) == 0xC0) {
			// pointer
			uint8_t offset = ((code & 0x3F) << 8) | *it++;
			auto offset_it = buf + offset;
			return res + read_dns_name(buf, offset_it);
		} else if (!(code & 0xC0)) {
			if (!code)
				break;

			for (int i = 0; i < code; i++)
				res += (*it++);

			if (*it)
				res += '.';
		} else {
			break;
		}
	}

	return res;
}

int lookup_name_dns(struct lookup_result &buf, const char *name,
		frg::string<MemoryAllocator> &canon_name, int family) {
	frg::string<MemoryAllocator> request{getAllocator()};

	int num_q = 1;
	struct dns_header header;
	header.identification = htons(123);
	header.flags = htons(0x100);
	header.no_q = htons(num_q);
	header.no_ans = htons(0);
	header.no_auths = htons(0);
	header.no_additional = htons(0);

	request.resize(sizeof(header));
	memcpy(request.data(), &header, sizeof(header));

	const char *end = name;
	while (*end != '\0') {
		end = strchrnul(name, '.');
		size_t length = end - name;
		frg::string_view substring{name, length};
		name += length + 1;
		request += char(length);
		request += substring;
	}

	request += char(0);
	// set question type to fetch A or AAAA records
	uint16_t qtype = RECORD_A;
	if (family == AF_INET6)
		qtype = RECORD_AAAA;

	request += qtype >> 8;
	request += qtype & 0xFF;
	// set CLASS to IN
	request += 0;
	request += 1;

	mlibc::service_result serv_buf{getAllocator()};
	int serv_count = mlibc::lookup_serv_by_name(serv_buf, "domain", IPPROTO_UDP, SOCK_DGRAM, 0);
	if (serv_count < 0) {
		mlibc::infoLogger() << "mlibc: could not resolve DNS service" << frg::endlog;
		return -EAI_SERVICE;
	}

	struct sockaddr_in sin = {};
	sin.sin_family = AF_INET;
	sin.sin_port = htons(serv_buf[0].port);

	auto nameserver = get_nameserver();
	if (!inet_aton(nameserver ? nameserver->name.data() : "127.0.0.1", &sin.sin_addr)) {
		mlibc::infoLogger() << "lookup_name_dns(): inet_aton() failed!" << frg::endlog;
		return -EAI_SYSTEM;
	}

	int fd = socket(AF_INET, SOCK_DGRAM, 0);
	if (fd < 0) {
		mlibc::infoLogger() << "lookup_name_dns(): socket() failed" << frg::endlog;
		return -EAI_SYSTEM;
	}

	size_t sent = sendto(fd, request.data(), request.size(), 0,
			(struct sockaddr*)&sin, sizeof(sin));
	if (sent != request.size()) {
		mlibc::infoLogger() << "lookup_name_dns(): sendto() failed to send everything" << frg::endlog;
		return -EAI_SYSTEM;
	}

	char response[256];
	ssize_t rlen;
	int num_ans = 0;
	while ((rlen = recvfrom(fd, response, 256, 0, nullptr, nullptr)) >= 0) {
		if ((size_t)rlen < sizeof(struct dns_header))
			continue;
		auto response_header = reinterpret_cast<struct dns_header*>(response);
		if (response_header->identification != header.identification)
			return -EAI_FAIL;

		if ((ntohs(response_header->flags) & 0xF) == RETURN_NXDOMAIN)
			return -EAI_NONAME;

		auto it = response + sizeof(struct dns_header);
		for (int i = 0; i < ntohs(response_header->no_q); i++) {
			auto dns_name = read_dns_name(response, it);
			(void) dns_name;
			it += 4;
		}

		for (int i = 0; i < ntohs(response_header->no_ans); i++) {
			struct dns_addr_buf buffer;
			auto dns_name = read_dns_name(response, it);

			uint16_t rr_type = (it[0] << 8) | it[1];
			uint16_t rr_class = (it[2] << 8) | it[3];
			uint16_t rr_length = (it[8] << 8) | it[9];
			it += 10;
			(void)rr_class;

			switch (rr_type) {
				case RECORD_A:
					if (family != AF_UNSPEC && family != AF_INET)
						continue;

					memcpy(buffer.addr, it, rr_length);
					it += rr_length;
					buffer.family = AF_INET;
					buffer.name = std::move(dns_name);
					buf.buf.push(std::move(buffer));
					break;
				case RECORD_AAAA:
					if (family != AF_UNSPEC && family != AF_INET6)
						continue;

					memcpy(buffer.addr, it, rr_length);
					it += rr_length;
					buffer.family = AF_INET6;
					buffer.name = std::move(dns_name);
					buf.buf.push(std::move(buffer));
					break;
				case RECORD_CNAME:
					canon_name = read_dns_name(response, it);
					buf.aliases.push(std::move(dns_name));
					break;
				default:
					mlibc::infoLogger() << "lookup_name_dns: unknown rr type "
						<< rr_type << frg::endlog;
					break;
			}
		}
		num_ans += ntohs(response_header->no_ans);

		if (num_ans >= num_q)
			break;
	}

	close(fd);
	return buf.buf.size();
}

int lookup_addr_dns(frg::span<char> name, frg::array<uint8_t, 16> &addr, int family) {
	frg::string<MemoryAllocator> request{getAllocator()};

	int num_q = 1;
	struct dns_header header;
	header.identification = htons(123);
	header.flags = htons(0x100);
	header.no_q = htons(num_q);
	header.no_ans = htons(0);
	header.no_auths = htons(0);
	header.no_additional = htons(0);

	request.resize(sizeof(header));
	memcpy(request.data(), &header, sizeof(header));

	char addr_str[64];
	if(!inet_ntop(family, addr.data(), addr_str, sizeof(addr_str))) {
		switch(errno) {
			case EAFNOSUPPORT:
				return -EAI_FAMILY;
			case ENOSPC:
				return -EAI_OVERFLOW;
			default:
				return -EAI_FAIL;
		}
	}
	frg::string<MemoryAllocator> req_str{getAllocator(), addr_str};
	req_str += ".in-addr.arpa";

	frg::string_view req_view{req_str.data(), req_str.size()};
	size_t ptr = 0;
	do {
		size_t next = req_view.find_first('.', ptr);
		size_t length = next != (size_t)-1 ? next - ptr : req_view.size() - ptr;
		frg::string_view substring = req_view.sub_string(ptr, length);
		request += char(length);
		request += substring;
		ptr = next + 1;
	} while(ptr != 0);

	request += char(0);
	// set question type to fetch PTR records
	request += 0;
	request += 12;
	// set CLASS to IN
	request += 0;
	request += 1;

	mlibc::service_result serv_buf{getAllocator()};
	int serv_count = mlibc::lookup_serv_by_name(serv_buf, "domain", IPPROTO_UDP, SOCK_DGRAM, 0);
	if (serv_count < 0) {
		mlibc::infoLogger() << "mlibc: could not resolve DNS service" << frg::endlog;
		return -EAI_SERVICE;
	}

	struct sockaddr_in sin = {};
	sin.sin_family = AF_INET;
	sin.sin_port = htons(serv_buf[0].port);

	auto nameserver = get_nameserver();
	if (!inet_aton(nameserver ? nameserver->name.data() : "127.0.0.1", &sin.sin_addr)) {
		mlibc::infoLogger() << "lookup_name_dns(): inet_aton() failed!" << frg::endlog;
		return -EAI_SYSTEM;
	}

	int fd = socket(AF_INET, SOCK_DGRAM, 0);
	if (fd < 0) {
		mlibc::infoLogger() << "lookup_name_dns(): socket() failed" << frg::endlog;
		return -EAI_SYSTEM;
	}

	size_t sent = sendto(fd, request.data(), request.size(), 0,
			(struct sockaddr*)&sin, sizeof(sin));
	if (sent != request.size()) {
		mlibc::infoLogger() << "lookup_name_dns(): sendto() failed to send everything" << frg::endlog;
		return -EAI_SYSTEM;
	}

	char response[256];
	ssize_t rlen;
	int num_ans = 0;
	while ((rlen = recvfrom(fd, response, 256, 0, nullptr, nullptr)) >= 0) {
		if ((size_t)rlen < sizeof(struct dns_header))
			continue;
		auto response_header = reinterpret_cast<struct dns_header*>(response);
		if (response_header->identification != header.identification)
			return -EAI_FAIL;

		auto it = response + sizeof(struct dns_header);
		for (int i = 0; i < ntohs(response_header->no_q); i++) {
			auto dns_name = read_dns_name(response, it);
			(void) dns_name;
			it += 4;
		}

		for (int i = 0; i < ntohs(response_header->no_ans); i++) {
			struct dns_addr_buf buffer;
			auto dns_name = read_dns_name(response, it);

			uint16_t rr_type = (it[0] << 8) | it[1];
			uint16_t rr_class = (it[2] << 8) | it[3];
			uint16_t rr_length = (it[8] << 8) | it[9];
			it += 10;
			(void)rr_class;
			(void)rr_length;

			(void)dns_name;

			switch (rr_type) {
				case RECORD_PTR: {
					auto ptr_name = read_dns_name(response, it);
					if (ptr_name.size() >= name.size())
						return -EAI_OVERFLOW;
					std::copy(ptr_name.begin(), ptr_name.end(), name.data());
					name.data()[ptr_name.size()] = '\0';
					return 1;
				}
				default:
					mlibc::infoLogger() << "lookup_addr_dns: unknown rr type "
						<< rr_type << frg::endlog;
					break;
			}
			num_ans += ntohs(response_header->no_ans);

			if (num_ans >= num_q)
				break;
		}
	}

	close(fd);
	return 0;
}

int lookup_name_hosts(struct lookup_result &buf, const char *name,
		frg::string<MemoryAllocator> &canon_name, int family) {
	auto file = fopen("/etc/hosts", "r");
	if (!file) {
		switch (errno) {
			case ENOENT:
			case ENOTDIR:
			case EACCES:
				return -EAI_SERVICE;
			default:
				return -EAI_SYSTEM;
		}
	}

	char line[128];
	int name_length = strlen(name);
	while (fgets(line, 128, file)) {
		char *pos;
		// same way to deal with comments as in services.cpp
		if ((pos = strchr(line, '#'))) {
			*pos++ = '\n';
			*pos = '\0';
		}

		for(pos = line + 1; (pos = strstr(pos, name)) &&
				(!isspace(pos[-1]) || !isspace(pos[name_length])); pos++);
		if (!pos)
			continue;

		for (pos = line; !isspace(*pos); pos++);
		*pos = '\0';

		struct dns_addr_buf buffer;

		if ((family == AF_UNSPEC || family == AF_INET) && inet_pton(AF_INET, line, buffer.addr)) {
			buffer.family = AF_INET;
		} else if((family == AF_UNSPEC || family == AF_INET6) && inet_pton(AF_INET6, line, buffer.addr)) {
			buffer.family = AF_INET6;
		} else {
			continue; // not a valid address
		}

		pos++;
		for(; *pos && isspace(*pos); pos++);
		char *end;
		for(end = pos; *end && !isspace(*end); end++);

		buffer.name = frg::string<MemoryAllocator>{pos,
			static_cast<size_t>(end - pos), getAllocator()};
		canon_name = buffer.name;

		buf.buf.push(std::move(buffer));

		pos = end;
		while (pos[1]) {
			for (; *pos && isspace(*pos); pos++);
			for (end = pos; *end && !isspace(*end); end++);
			auto name = frg::string<MemoryAllocator>{pos,
				static_cast<size_t>(end - pos), getAllocator()};
			buf.aliases.push(std::move(name));
			pos = end;
		}
	}

	fclose(file);
	return buf.buf.size();
}

int lookup_addr_hosts(frg::span<char> name, frg::array<uint8_t, 16> &addr, int family) {
	auto file = fopen("/etc/hosts", "r");
	if (!file) {
		switch (errno) {
			case ENOENT:
			case ENOTDIR:
			case EACCES:
				return -EAI_SERVICE;
			default:
				return -EAI_SYSTEM;
		}
	}

	// Buffer to hold ASCII version of address
	char addr_str[64];
	if(!inet_ntop(family, addr.data(), addr_str, sizeof(addr_str))) {
		switch(errno) {
			case EAFNOSUPPORT:
				return -EAI_FAMILY;
			case ENOSPC:
				return -EAI_OVERFLOW;
			default:
				return -EAI_FAIL;
		}
	}
	int addr_str_len = strlen(addr_str);

	char line[128];
	while (fgets(line, 128, file)) {
		char *pos;
		// same way to deal with comments as in services.cpp
		if ((pos = strchr(line, '#'))) {
			*pos++ = '\n';
			*pos = '\0';
		}
		if (strncmp(line, addr_str, addr_str_len))
			continue;

		for (pos = line + addr_str_len + 1; isspace(*pos); pos++);
		char *begin = pos;
		for (; !isspace(*pos); pos++);
		char *end = pos;

		size_t size = end - begin;
		if (size >= name.size())
			return -EAI_OVERFLOW;
		std::copy(begin, end, name.data());
		name.data()[size] = '\0';
		return 1;
	}
	return 0;
}

int lookup_name_null(struct lookup_result &buf, int flags, int family) {
	if (flags & AI_PASSIVE) {
		if (family != AF_INET6) {
			struct dns_addr_buf addr_buf;
			addr_buf.family = AF_INET;

			in_addr_t addr = INADDR_ANY;
			memcpy(&addr_buf.addr, &addr, 4);

			buf.buf.push_back(addr_buf);
		}
		if (family != AF_INET) {
			struct dns_addr_buf addr_buf;
			addr_buf.family = AF_INET6;

			struct in6_addr addr = IN6ADDR_ANY_INIT;
			memcpy(&addr_buf.addr, &addr, 16);

			buf.buf.push_back(addr_buf);
		}
	} else {
		if (family != AF_INET6) {
			struct dns_addr_buf addr_buf;
			addr_buf.family = AF_INET;

			in_addr_t addr = INADDR_LOOPBACK;
			memcpy(&addr_buf.addr, &addr, 4);

			buf.buf.push_back(addr_buf);
		}
		if (family != AF_INET) {
			struct dns_addr_buf addr_buf;
			addr_buf.family = AF_INET6;

			struct in6_addr addr = IN6ADDR_LOOPBACK_INIT;
			memcpy(&addr_buf.addr, &addr, 16);

			buf.buf.push_back(addr_buf);
		}
	}
	return buf.buf.size();
}

int lookup_name_ip(struct lookup_result &buf, const char *name, int family) {
	if (family == AF_INET) {
		in_addr_t addr = 0;
		int res = inet_pton(AF_INET, name, &addr);

		if (res <= 0)
			return -EAI_NONAME;

		struct dns_addr_buf addr_buf;
		addr_buf.family = AF_INET;
		memcpy(&addr_buf.addr, &addr, 4);

		buf.buf.push_back(addr_buf);
		return 1;
	}

	if (family == AF_INET6) {
		struct in6_addr addr{};
		int res = inet_pton(AF_INET6, name, &addr);

		if (res <= 0)
			return -EAI_NONAME;

		struct dns_addr_buf addr_buf;
		addr_buf.family = AF_INET6;
		memcpy(&addr_buf.addr, &addr, 16);

		buf.buf.push_back(addr_buf);
		return 1;
	}

	// If no family was specified we try ipv4 and then ipv6.
	in_addr_t addr4 = 0;
	int res = inet_pton(AF_INET, name, &addr4);

	if (res > 0) {
		struct dns_addr_buf addr_buf;
		addr_buf.family = AF_INET;
		memcpy(&addr_buf.addr, &addr4, 4);

		buf.buf.push_back(addr_buf);
		return 1;
	}

	struct in6_addr addr6{};
	res = inet_pton(AF_INET6, name, &addr6);

	if (res <= 0)
		return -EAI_NONAME;

	struct dns_addr_buf addr_buf;
	addr_buf.family = AF_INET6;
	memcpy(&addr_buf.addr, &addr6, 16);

	buf.buf.push_back(addr_buf);
	return 1;
}

} // namespace mlibc
