#include "ppsspp_config.h"

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <map>

#include "Common/Log.h"
#include "Common/TimeUtil.h"
#include "Common/StringUtils.h"
#include "Common/Data/Encoding/Utf8.h"
#include "Common/Net/SocketCompat.h"
#include "Common/Net/Resolve.h"

#ifndef HTTPS_NOT_AVAILABLE
#include "ext/naett/naett.h"
#endif

#if PPSSPP_PLATFORM(ANDROID)
#include <jni.h>
extern JavaVM *gJvm;
#endif

namespace net {

static bool g_naettInitialized;
static bool g_wsaInitialized;

void Init() {
#ifdef _WIN32
	// WSA does its own internal reference counting, no need to keep track of if we inited or not.
	WSADATA wsaData{};
	if (FAILED(WSAStartup(MAKEWORD(2, 2), &wsaData))) {
		ERROR_LOG(Log::sceNet, "WSAStartup failed");
	} else {
		g_wsaInitialized = true;
	}
#endif
	if (!g_naettInitialized) {
#ifndef HTTPS_NOT_AVAILABLE
#if PPSSPP_PLATFORM(ANDROID)
		_assert_(gJvm != nullptr);
		naettInit(gJvm);
#else
		naettInit(NULL);
#endif
#endif
		g_naettInitialized = true;
	}
}

void Shutdown() {
#ifdef _WIN32
	if (g_wsaInitialized) {
		WSACleanup();
	}
#endif
}

// This checks whether a TCP connection to host : port can be established within a given timeout.
// It does this by attempting a new, non - blocking TCP connect() and waiting for it to succeed or time out.
// The socket is closed immediately after the check.
// It does not inspect or detect existing connections from other applications; it only tests reachability by making its own connection attempt.
bool HostPortExists(const std::string &host, int port, int timeout_ms) {
	if (host.empty() || (port <= 0 || port > 65535) || timeout_ms < 0) return false;

	addrinfo hints;
	addrinfo* res = nullptr;

	std::memset(&hints, 0, sizeof(hints));
	hints.ai_socktype = SOCK_STREAM; // TCP
	hints.ai_family = AF_UNSPEC;     // IPv4 or IPv6

	int gai = getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, &res);
	if (gai != 0) {
		// getaddrinfo failed (DNS resolve failed or bad port)
		return false;
	}

	bool ok = false;

	for (addrinfo* p = res; p != nullptr && !ok; p = p->ai_next) {
		// create socket
		int sockfd =
#ifdef _WIN32
		(int)socket(p->ai_family, p->ai_socktype, p->ai_protocol);
#else
			socket(p->ai_family, p->ai_socktype, p->ai_protocol);
#endif
		if (sockfd < 0) {
			continue;
		}

		// make non-blocking
#ifdef _WIN32
		unsigned long mode = 1;
		ioctlsocket((SOCKET)sockfd, FIONBIO, &mode);
#else
		// On non-Windows, check if fd is too large for select()
		if (sockfd >= FD_SETSIZE) {
			close(sockfd);
			continue;
		}
		int flags = fcntl(sockfd, F_GETFL, 0);
		if (flags == -1) flags = 0;
		fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
#endif

		// try connect
		int conn = connect(sockfd, p->ai_addr, (int)p->ai_addrlen);
#ifdef _WIN32
		if (conn == 0) {
			ok = true; // immediate success
		}
		else {
			int err = WSAGetLastError();
			if (err == WSAEWOULDBLOCK || err == WSAEINPROGRESS) {
				// fall through to select
			}
			else {
				// immediate failure
			}
		}
#else
		if (conn == 0) {
			ok = true; // immediate success
		}
		else {
			if (errno == EINPROGRESS) {
				// fall through to select
			}
			else {
				// immediate failure
			}
		}
#endif

		if (!ok) {
			// wait for writable with timeout
			fd_set writefds;
			FD_ZERO(&writefds);
#ifdef _WIN32
			FD_SET((SOCKET)sockfd, &writefds);
#else
			FD_SET(sockfd, &writefds);
#endif

			fd_set exceptfds;
			FD_ZERO(&exceptfds);
#ifdef _WIN32
			FD_SET((SOCKET)sockfd, &exceptfds);
#else
			FD_SET(sockfd, &exceptfds);
#endif

			timeval tv;
			tv.tv_sec = timeout_ms / 1000;
			tv.tv_usec = (timeout_ms % 1000) * 1000;

			int sel = select(
#ifdef _WIN32
				0,
#else
				sockfd + 1,
#endif
				nullptr, &writefds, &exceptfds, &tv);

			if (sel > 0) {
				// check for error on socket
				int sock_err = 0;
				socklen_t len = sizeof(sock_err);
#ifdef _WIN32
				int ret = getsockopt((SOCKET)sockfd, SOL_SOCKET, SO_ERROR, (char*)&sock_err, &len);
#else
				int ret = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &sock_err, &len);
#endif
#ifdef _WIN32
				bool writable = FD_ISSET(static_cast<SOCKET>(sockfd), &writefds) != 0;
#else
				bool writable = FD_ISSET(sockfd, &writefds) != 0;
#endif

				if (ret == 0 && sock_err == 0 && writable) {
					ok = true;
				}
			}
			// else timeout or error -> try next addr
		}

		// close socket
#ifdef _WIN32
		closesocket((SOCKET)sockfd);
#else
		close(sockfd);
#endif
	}

	freeaddrinfo(res);
	return ok;
}

// NOTE: Due to the nature of getaddrinfo, this can block indefinitely. Not good.
bool DNSResolve(const std::string &host, const std::string &service, addrinfo **res, std::string &error, DNSType type) {
#if PPSSPP_PLATFORM(SWITCH)
	// Force IPv4 lookups.
	if (type == DNSType::IPV6) {
		*res = nullptr;
		return false;
	} else if (type == DNSType::ANY) {
		type = DNSType::IPV4;
	}
#endif

	addrinfo hints = {0};
	// TODO: Might be uses to lookup other values.
	hints.ai_socktype = SOCK_STREAM;
#ifdef __ANDROID__
	hints.ai_flags = AI_ADDRCONFIG;
#else
	// AI_V4MAPPED seems to have issues on some platforms, not sure we should include it:
	// http://stackoverflow.com/questions/1408030/what-is-the-purpose-of-the-ai-v4mapped-flag-in-getaddrinfo
	hints.ai_flags = /*AI_V4MAPPED |*/ AI_ADDRCONFIG;
#endif
	hints.ai_protocol = 0;
	if (type == DNSType::IPV4)
		hints.ai_family = AF_INET;
	else if (type == DNSType::IPV6)
		hints.ai_family = AF_INET6;

	const char *servicep = service.empty() ? nullptr : service.c_str();

	*res = nullptr;
	int result = getaddrinfo(host.c_str(), servicep, &hints, res);
	if (result == EAI_AGAIN) {
		// Temporary failure.  Since this already blocks, let's just try once more.
		sleep_ms(1, "dns-resolve-poll");
		result = getaddrinfo(host.c_str(), servicep, &hints, res);
	}

	if (result != 0) {
#ifdef _WIN32
		error = ConvertWStringToUTF8(gai_strerror(result));
#else
		error = gai_strerror(result);
#endif
		if (*res != nullptr)
			freeaddrinfo(*res);
		*res = nullptr;
		return false;
	}

	return true;
}

void DNSResolveFree(addrinfo *res)
{
	if (res)
		freeaddrinfo(res);
}

bool GetLocalIP4List(std::vector<std::string> &IP4s) {
	char ipstr[INET6_ADDRSTRLEN]; // We use IPv6 length since it's longer than IPv4
// getifaddrs first appeared in glibc 2.3, On Android officially supported since __ANDROID_API__ >= 24
#if defined(_IFADDRS_H_) || (__GLIBC__ > 2) || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 3) || (__ANDROID_API__ >= 24)
	INFO_LOG(Log::sceNet, "GetIPList from getifaddrs");
	struct ifaddrs* ifAddrStruct = NULL;
	struct ifaddrs* ifa = NULL;

	getifaddrs(&ifAddrStruct);
	if (ifAddrStruct != NULL) {
		for (ifa = ifAddrStruct; ifa != NULL; ifa = ifa->ifa_next) {
			if (!ifa->ifa_addr) {
				continue;
			}
			if (ifa->ifa_addr->sa_family == AF_INET) {
				// is a valid IP4 Address
				if (inet_ntop(AF_INET, &((struct sockaddr_in*)ifa->ifa_addr)->sin_addr, ipstr, sizeof(ipstr)) != 0) {
					IP4s.push_back(ipstr);
				}
			}
			/*else if (ifa->ifa_addr->sa_family == AF_INET6) {
				// is a valid IP6 Address
				if (inet_ntop(AF_INET6, &((struct sockaddr_in6*)ifa->ifa_addr)->sin6_addr, ipstr, sizeof(ipstr)) != 0) {
					IP6s.push_back(ipstr);
				}
			}*/
		}

		freeifaddrs(ifAddrStruct);
		return true;
	}
#elif defined(SIOCGIFCONF) // Better detection on Linux/UNIX/MacOS/some Android
	INFO_LOG(Log::IO, "GetIPList from SIOCGIFCONF");
	static struct ifreq ifreqs[32];
	struct ifconf ifc{};
	ifc.ifc_req = ifreqs;
	ifc.ifc_len = sizeof(ifreqs);

	int sd = socket(AF_INET, SOCK_DGRAM, 0);
	if (sd < 0) {
		ERROR_LOG(Log::IO, "GetIPList failed to create socket (result = %i, errno = %i)", sd, socket_errno);
		return false;
	}

	int r = ioctl(sd, SIOCGIFCONF, (char*)&ifc);
	if (r != 0) {
		ERROR_LOG(Log::IO, "GetIPList failed ioctl/SIOCGIFCONF (result = %i, errno = %i)", r, socket_errno);
		return false;
	}

	struct ifreq* item;
	struct sockaddr* addr;

	for (int i = 0; i < ifc.ifc_len / sizeof(struct ifreq); ++i)
	{
		item = &ifreqs[i];
		addr = &(item->ifr_addr);

		// Get the IP address
		r = ioctl(sd, SIOCGIFADDR, item);
		if (r != 0)
		{
			ERROR_LOG(Log::IO, "GetIPList failed ioctl/SIOCGIFADDR (i = %i, result = %i, errno = %i)", i, r, socket_errno);
		}

		if (ifreqs[i].ifr_addr.sa_family == AF_INET) {
			// is a valid IP4 Address
			if (inet_ntop(AF_INET, &((struct sockaddr_in*)addr)->sin_addr, ipstr, sizeof(ipstr)) != 0) {
				IP4s.emplace_back(ipstr);
			}
		}
		/*else if (ifreqs[i].ifr_addr.sa_family == AF_INET6) {
			// is a valid IP6 Address
			if (inet_ntop(AF_INET6, &((struct sockaddr_in6*)addr)->sin6_addr, ipstr, sizeof(ipstr)) != 0) {
				IP6s.push_back(ipstr);
			}
		}*/
	}

	close(sd);
	return true;
#else // Fallback to POSIX/Cross-platform way but may not work well on Linux (ie. only shows 127.0.0.1)
	DEBUG_LOG(Log::IO, "GetIPList from fallback method");
	struct addrinfo hints, * res, * p;
	memset(&hints, 0, sizeof hints);
	hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version
	hints.ai_socktype = SOCK_DGRAM;

	// Get local host name
	char szHostName[256] = "";
	if (::gethostname(szHostName, sizeof(szHostName))) {
		// Error handling
	}

	int status;
	if ((status = getaddrinfo(szHostName, NULL, &hints, &res)) != 0) {
		//fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
		return false;
	}
	for (p = res; p != NULL; p = p->ai_next) {
		if (p->ai_family == AF_INET) {
			// is a valid IP4 Address
			if (inet_ntop(p->ai_family, &(((struct sockaddr_in*)p->ai_addr)->sin_addr), ipstr, sizeof(ipstr)) != 0) {
				IP4s.push_back(ipstr);
			}
		}
		/*else if (p->ai_family == AF_INET6) {
			// is a valid IP6 Address
			if (inet_ntop(p->ai_family, &(((struct sockaddr_in6*)p->ai_addr)->sin6_addr), ipstr, sizeof(ipstr)) != 0) {
				IP6s.push_back(ipstr);
			}
		}*/
	}

	freeaddrinfo(res); // free the linked list
	return true;
#endif
	return false;
}

// IP address parser
int inet_pton(int af, const char* src, void* dst)
{
	if (af == AF_INET)
	{
		unsigned char *ip = (unsigned char *)dst;
		int k = 0, x = 0;
		char ch;
		for (int i = 0; (ch = src[i]) != 0; i++)
		{
			if (ch == '.')
			{
				ip[k++] = x;
				if (k == 4)
					return 0;
				x = 0;
			}
			else if (ch < '0' || ch > '9')
				return 0;
			else
				x = x * 10 + ch - '0';
			if (x > 255)
				return 0;
		}
		ip[k++] = x;
		if (k != 4)
			return 0;
	}
	else if (af == AF_INET6)
	{
		unsigned short* ip = ( unsigned short* )dst;
		int i;
		for (i = 0; i < 8; i++) ip[i] = 0;
		int k = 0;
		unsigned int x = 0;
		char ch;
		int marknum = 0;
		for (i = 0; src[i] != 0; i++)
		{
			if (src[i] == ':')
				marknum++;
		}
		for (i = 0; (ch = src[i]) != 0; i++)
		{
			if (ch == ':')
			{
				x = ((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8);
				ip[k++] = x;
				if (k == 8)
					return 0;
				x = 0;
				if (i > 0 && src[i - 1] == ':')
					k += 7 - marknum;
			}
			else if (ch >= '0' && ch <= '9')
				x = x * 16 + ch - '0';
			else if (ch >= 'a' && ch <= 'f')
				x = x * 16 + ch - 'a' + 10;
			else if (ch >= 'A' && ch <= 'F')
				x = x * 16 + ch - 'A' + 10;
			else
				return 0;
			if (x > 0xFFFF)
				return 0;
		}
		x = ((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8);
		ip[k++] = x;
		if (k != 8)
			return 0;
	}
	return 1;
}

// Structs for implementing DNS are available here:
// https://web.archive.org/web/20201204080751/https://www.binarytides.com/dns-query-code-in-c-with-winsock/

#define DNS_PORT 53
#define DNS_QUERY_TYPE_A 1
#define DNS_QUERY_CLASS_IN 1

// DNS header structure
struct DNSHeader {
	uint16_t id;       // Identifier
	uint16_t flags;    // Flags
	uint16_t q_count;  // Number of questions
	uint16_t ans_count;  // Number of answers
	uint16_t auth_count; // Number of authority records
	uint16_t add_count;  // Number of additional records
};

// Function to convert a domain name to DNS query format
// http://www.tcpipguide.com/free/t_DNSNameNotationandMessageCompressionTechnique.htm
static void encode_domain_name(const char *domain, unsigned char *encoded) {
	const char *pos = domain;
	unsigned char *ptr = encoded;

	while (*pos) {
		const char *start = pos;
		while (*pos && *pos != '.') {
			pos++;
		}

		*ptr++ = (unsigned char)(pos - start);  // length field
		memcpy(ptr, start, pos - start);
		ptr += pos - start;

		if (*pos == '.') {
			pos++;
		}
	}
	*ptr = 0; // End of domain name
}

// Function to parse and print the DNS response
static bool parse_dns_response(unsigned char *buffer, size_t response_len, uint32_t *output) {
	DNSHeader *dns = (DNSHeader *)buffer;
	unsigned char *ptr = buffer + sizeof(struct DNSHeader);

	DEBUG_LOG(Log::sceNet, "DNS Response:");
	DEBUG_LOG(Log::sceNet, "ID: 0x%x", ntohs(dns->id));
	DEBUG_LOG(Log::sceNet, "Flags: 0x%x", ntohs(dns->flags));
	DEBUG_LOG(Log::sceNet, "Questions: %d", ntohs(dns->q_count));
	DEBUG_LOG(Log::sceNet, "Answers: %d", ntohs(dns->ans_count));
	DEBUG_LOG(Log::sceNet, "Authority Records: %d", ntohs(dns->auth_count));
	DEBUG_LOG(Log::sceNet, "Additional Records: %d", ntohs(dns->add_count));

	// Skip over the question section
	const int q_count = ntohs(dns->q_count);
	for (int i = 0; i < q_count; i++) {
		while (*ptr != 0) {
			ptr += (*ptr) + 1;
		}
		ptr += 5; // Null byte + QTYPE (2 bytes) + QCLASS (2 bytes)
	}

	*output = 0;

	// Parse the answer section
	const int ans_count = ntohs(dns->ans_count);
	for (int i = 0; i < ans_count; i++) {
		DEBUG_LOG(Log::sceNet, "Answer %d:\n", i + 1);

		// Skip the name (can be a pointer or a sequence)
		if ((*ptr & 0xC0) == 0xC0) {
			ptr += 2; // Pointer (2 bytes)
		} else {
			while (*ptr != 0) ptr += (*ptr) + 1;
			ptr++;
		}

		// TODO: Use a struct or something.
		uint16_t type = ntohs(*((uint16_t *)ptr));
		ptr += 2;
		uint16_t clazz = ntohs(*((uint16_t *)ptr));
		ptr += 2;
		uint32_t ttl = ntohl(*((uint32_t *)ptr));
		ptr += 4;
		uint16_t data_len = ntohs(*((uint16_t *)ptr));
		ptr += 2;

		DEBUG_LOG(Log::sceNet, "  Type: %d", type);
		DEBUG_LOG(Log::sceNet, "  Class: %d", clazz);
		DEBUG_LOG(Log::sceNet, "  TTL: %u", ttl);
		DEBUG_LOG(Log::sceNet, "  Data length: %d", (int)data_len);

		if (type == DNS_QUERY_TYPE_A && data_len == 4) {
			// IPv4 address
			char ip[INET_ADDRSTRLEN];
			inet_ntop(AF_INET, ptr, ip, sizeof(ip));
			DEBUG_LOG(Log::sceNet, "  IPV4 Address: %s", ip);
			memcpy(output, ptr, 4);
			// Skipping further responses.
			return true;
		}

		ptr += data_len;
	}
	return false;
}

// This was written by ChatGPT, although not much of that remains, after all the cleanup and fixing...

// Specialized cache for the direct DNS lookups
struct DNSCacheEntry {
	uint32_t ipv4Address;
};

static std::map<std::string, DNSCacheEntry> g_directDNSCache;

bool DirectDNSLookupIPV4(const char *dns_server_ip, const char *domain, uint32_t *ipv4_addr) {
	if (!strlen(dns_server_ip)) {
		WARN_LOG(Log::sceNet, "Direct lookup: DNS server not specified");
		return false;
	}

	if (!strlen(domain)) {
		ERROR_LOG(Log::sceNet, "Direct lookup: Can't look up an empty domain");
		return false;
	}

	std::string key = StringFromFormat("%s:%s", dns_server_ip, domain);

	auto iter = g_directDNSCache.find(key);
	if (iter != g_directDNSCache.end()) {
		INFO_LOG(Log::sceNet, "Returning cached response from direct DNS request for '%s' to DNS server '%s", domain, dns_server_ip);
		*ipv4_addr = iter->second.ipv4Address;
		return true;
	}

	SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	// Create UDP socket
	if (sockfd < 0) {
		ERROR_LOG(Log::sceNet, "Socket creation for direct DNS failed");
		return false;
	}

	struct sockaddr_in server_addr{};
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(DNS_PORT);

	if (net::inet_pton(AF_INET, dns_server_ip, &server_addr.sin_addr) <= 0) {
		ERROR_LOG(Log::sceNet,"Invalid DNS server IP address %s", dns_server_ip);
		closesocket(sockfd);
		return false;
	}

	// Build DNS query
	unsigned char buffer[1024]{};
	struct DNSHeader *dns = (struct DNSHeader *)buffer;
	dns->id = htons(0x1234);  // Random ID
	dns->flags = htons(0x0100); // Standard query
	dns->q_count = htons(1);    // One question

	unsigned char *qname = buffer + sizeof(DNSHeader);
	encode_domain_name(domain, qname);

	unsigned char *qinfo = qname + strlen((const char *)qname) + 1;
	*((uint16_t *)qinfo) = htons(DNS_QUERY_TYPE_A); // Query type: A
	*((uint16_t *)(qinfo + 2)) = htons(DNS_QUERY_CLASS_IN); // Query class: IN

	// Send DNS query
	size_t query_len = sizeof(DNSHeader) + (qinfo - buffer) + 4;
	if (sendto(sockfd, (const char *)buffer, (int)query_len, 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
		ERROR_LOG(Log::sceNet, "Failed to send DNS query");
		closesocket(sockfd);
		return false;
	}

	// Receive DNS response
	socklen_t server_len = sizeof(server_addr);
	size_t response_len;
	if ((response_len = recvfrom(sockfd, (char *)buffer, sizeof(buffer), 0, (struct sockaddr *)&server_addr, &server_len)) < 0) {
		ERROR_LOG(Log::sceNet, "Failed to receive DNS response");
		closesocket(sockfd);
		return false;
	}

	// Close socket
	closesocket(sockfd);

	// Done communicating, time to parse.
	if (!parse_dns_response(buffer, response_len, ipv4_addr)) {
		return false;
	}

	g_directDNSCache[key] = DNSCacheEntry{ *ipv4_addr };
	return true;
}

}  // namespace net
