//
// Copyright (c) 2014 ANSCENTER. All rights reserved.
//

#include "precomp.h"

#ifdef _WIN32

#include "wmihelper.h"

#else // !_WIN32

#if defined(__i386__) || defined(__amd64__)
#include <cpuid.h>
#endif

#include <sys/types.h>
#include <sys/socket.h>
#include <ifaddrs.h>
#include <netinet/in.h>
#include <net/if.h>
#ifdef __linux__
#    include <sys/ioctl.h>
#    include <netinet/in.h>
#    include <unistd.h>
#    include <string.h>

//    Match Linux to FreeBSD
#    define AF_LINK AF_PACKET
#else
#    include <net/if_dl.h>
#endif
#endif

#include <list>
#include <string>
#include <functional>
#include <algorithm>
#include <cctype>
#include <stdio.h>
#include <set>

#include "bitstream2.h"
#include "bitstream3.h"
#include "base32.h"
#include "crc32.h"

#include "hwid.h"
#include "except.h"
#include "uniconv.h"

using namespace std;

string HardwareId::hardwareId;

#ifndef _WIN32

const sockaddr_in* castToIP4(const sockaddr* addr) {
	if (addr == NULL) {
		return NULL;
	}
	else if (addr->sa_family == AF_INET) {
		// An IPv4 address
		return reinterpret_cast<const sockaddr_in*>(addr);
	}
	else {
		// Not an IPv4 address
		return NULL;
	}
}

void GetCPUIDPropertyList(std::list<std::string>* propList)
{
	unsigned int level = 1, eax = 0, ebx, ecx, edx = 0;

#if defined(__i386__) || defined(__amd64__)
	__get_cpuid(level, &eax, &ebx, &ecx, &edx);
#endif

	char buf[17];
	sprintf(buf, "%08X%08X", edx, eax);
	propList->push_back(buf);
}

void GetMACPropertyList(std::list<std::string>* propList, int maxCount)
{
	// Head of the interface address linked list
	ifaddrs* ifap = NULL;

	int r = getifaddrs(&ifap);

	if (r != 0) {
		return;
	}

	ifaddrs* current = ifap;

	if (current == NULL) {
		return;
	}

	while (current != NULL && maxCount > 0) {

		const sockaddr_in* interfaceAddress = castToIP4(current->ifa_addr);
		const sockaddr_in* broadcastAddress = castToIP4(current->ifa_dstaddr);
		const sockaddr_in* subnetMask = castToIP4(current->ifa_netmask);

		if ((current->ifa_addr != NULL) && (current->ifa_addr->sa_family == AF_LINK)) {
#ifdef __linux__
			struct ifreq ifr;

			int fd = socket(AF_INET, SOCK_DGRAM, 0);

			ifr.ifr_addr.sa_family = AF_INET;
			strcpy(ifr.ifr_name, current->ifa_name);
			ioctl(fd, SIOCGIFHWADDR, &ifr);
			close(fd);

			uint8_t* MAC = reinterpret_cast<uint8_t*>(ifr.ifr_hwaddr.sa_data);

#else // Posix/FreeBSD/Mac OS

			sockaddr_dl* sdl = (struct sockaddr_dl*)current->ifa_addr;
			uint8_t* MAC = reinterpret_cast<uint8_t*>(LLADDR(sdl));
#endif
			if (MAC[0] != 0 || MAC[1] != 0 || MAC[2] != 0 || MAC[3] != 0 || MAC[4] != 0 || MAC[5] != 0)
			{
				char buf[18];
				sprintf(buf, "%02x:%02x:%02x:%02x:%02x:%02x", MAC[0], MAC[1], MAC[2], MAC[3], MAC[4], MAC[5]);
				propList->push_back(buf);
				maxCount--;
			}
		}

		current = current->ifa_next;
	}

	freeifaddrs(ifap);
	ifap = NULL;
}

void GetHDDPropertyList(std::list<std::string>* propList, int maxCount)
{

}

#endif

unsigned short HardwareId::HashString(const char* str)
{
	unsigned int crc = Crc32::Compute((const unsigned char*)str, strlen(str));

	return (unsigned short)(crc >> 16);
}

unsigned short HardwareId::HashInt(int val)
{
	unsigned char buf[4] = { (unsigned char)(val >> 24), (unsigned char)(val >> 16), (unsigned char)(val >> 8), (unsigned char)(val & 0xFF) };
	unsigned int crc = Crc32::Compute(buf, 4);

	return (unsigned short)(crc >> 16);
}

// LOCALE-INDEPENDENT trim from start (in place)
static inline void ltrim(std::string& s) {
	s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
		return ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r' && ch != 0xA0;
		}));
}

// LOCALE-INDEPENDENT trim from end (in place)
static inline void rtrim(std::string& s) {
	s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
		return ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r' && ch != 0xA0;
		}).base(), s.end());
}

// trim from both ends (in place)
static inline void trim(std::string& s) {
	ltrim(s);
	rtrim(s);
}

// LOCALE-INDEPENDENT uppercase conversion
static inline void toUpperInvariant(std::string& s) {
	for (char& c : s) {
		if (c >= 'a' && c <= 'z') {
			c = c - 32;
		}
	}
}

// Normalize hardware string for consistent hashing
static std::string NormalizeHardwareString(const std::string& input) {
	std::string s = input;

	// Trim all types of whitespace
	trim(s);

	// Remove all remaining whitespace characters
	s.erase(std::remove_if(s.begin(), s.end(), [](unsigned char c) {
		return c <= 32 || c == 0xA0;  // Remove control chars and non-breaking space
		}), s.end());

	// Locale-independent uppercase for ASCII
	toUpperInvariant(s);

	// Remove non-ASCII characters (Korean text, etc.)
	s.erase(std::remove_if(s.begin(), s.end(), [](unsigned char c) {
		return c > 127;
		}), s.end());

	return s;
}

const char* HardwareId::GetCurrentHardwareId()
{
#ifdef _WIN32

	WmiHelper wmi;
#endif

	BitStream3 bits(125);
	list<string> propList;
	unsigned char version = 1;
	unsigned short hash1, hash2, hash3, hash4;
	unsigned short h0 = HashInt(0);

	bits.Write(&version, 3);

	propList.clear();

#ifdef _WIN32

	wmi.GetPropertyList("SELECT * FROM Win32_ComputerSystemProduct", "UUID", &propList);

	hash1 = h0;

	string s;

	for (list<string>::iterator iter = propList.begin(); iter != propList.end(); iter++)
	{
		s = NormalizeHardwareString(*iter);

		if ((s.length() > 1)
			&& (s != "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
			&& (s != "00000000000000000000000000000000"))
		{
			hash1 = HashString(s.c_str());
		}
	}

#else
	propList.push_back("4294967296");
#endif

	bits.WriteUInt16(hash1);

	// CPU IDs - get up to 4 for better resilience
	propList.clear();

#ifdef _WIN32
	wmi.GetPropertyList("SELECT ProcessorId FROM Win32_Processor", "ProcessorId", &propList, 4);
#else
	GetCPUIDPropertyList(&propList);
#endif

	// Normalize CPU IDs
	for (auto& cpu : propList) {
		cpu = NormalizeHardwareString(cpu);
	}

	hash1 = (propList.size() > 0) ? HashString(propList.front().c_str()) : h0;
	hash2 = (propList.size() > 1) ? HashString(propList.back().c_str()) : h0;

	bits.WriteUInt16(hash1);
	bits.WriteUInt16(hash2);

	// MAC Addresses - get MORE adapters and sort for consistency
	propList.clear();

#ifdef _WIN32
	// Try physical PCI adapters first
	wmi.GetPropertyList("SELECT MACAddress FROM Win32_NetworkAdapter WHERE (AdapterType = \"Ethernet 802.3\") AND (MACAddress IS NOT NULL) AND PNPDeviceId LIKE \"%PCI%\"", "MACAddress", &propList, 6);

	// Fallback to VMBUS for VMs
	if (propList.size() == 0)
	{
		wmi.GetPropertyList("SELECT MACAddress FROM Win32_NetworkAdapter WHERE (AdapterType = \"Ethernet 802.3\") AND (MACAddress IS NOT NULL) AND PNPDeviceId LIKE \"%VMBUS%\"", "MACAddress", &propList, 6);
	}
#else
	GetMACPropertyList(&propList, 6);
#endif

	// Normalize and sort MAC addresses for consistent ordering
	for (auto& mac : propList) {
		mac = NormalizeHardwareString(mac);
	}
	propList.sort();

	// Store up to 4 MAC hashes
	auto macIter = propList.begin();
	hash1 = (propList.size() > 0 && macIter != propList.end()) ? HashString((macIter++)->c_str()) : h0;
	hash2 = (propList.size() > 1 && macIter != propList.end()) ? HashString((macIter++)->c_str()) : h0;
	hash3 = (propList.size() > 2 && macIter != propList.end()) ? HashString((macIter++)->c_str()) : h0;
	hash4 = (propList.size() > 3 && macIter != propList.end()) ? HashString((macIter++)->c_str()) : h0;

	bits.WriteUInt16(hash1);
	bits.WriteUInt16(hash2);
	bits.WriteUInt16(hash3);
	bits.WriteUInt16(hash4);

	// HDD Serial Numbers - get more drives and sort
	propList.clear();

#ifdef _WIN32
	wmi.GetPropertyList("SELECT SerialNumber FROM Win32_PhysicalMedia WHERE SerialNumber IS NOT NULL", "SerialNumber", &propList, 4);
#else
	GetHDDPropertyList(&propList, 4);
#endif

	// Normalize and sort HDD serials
	for (auto& serial : propList) {
		serial = NormalizeHardwareString(serial);
	}
	propList.sort();

	auto hddIter = propList.begin();
	hash1 = (propList.size() > 0 && hddIter != propList.end()) ? HashString((hddIter++)->c_str()) : h0;
	hash2 = (propList.size() > 1 && hddIter != propList.end()) ? HashString((hddIter++)->c_str()) : h0;

	bits.WriteUInt16(hash1);
	bits.WriteUInt16(hash2);

	unsigned char* rawHwid = bits.GetBuffer();

	BASE32 base32;

	auto encHwid = base32.encode(rawHwid, 16);

	hardwareId = encHwid.c_str();

	hardwareId.erase(hardwareId.length() - 1);

	unsigned i;
	unsigned insertPos;
	// separate character groups
	for (i = 0, insertPos = 5; i < 4; i++)
	{
		hardwareId.insert(insertPos, "-");
		insertPos += 6;
	}

	return hardwareId.c_str();
}

bool HardwareId::MatchCurrentHardwareId(const char* hwid)
{
#ifdef _WIN32
	WmiHelper wmi;
#endif

	unsigned short h0 = HashInt(0);
	string hardwareId((char*)hwid);

	for (int i = 0, erasePos = 0; i < 4; i++)
	{
		erasePos += 5;
		hardwareId.erase(erasePos, 1);
	}

	BASE32 base32;
	int bufLen;

	int padLen;
	int len = base32.encode_pad_length(((int)hardwareId.length() * 5 + 7) >> 3, &padLen);

	if (len > (int)hardwareId.length())
		hardwareId.append(len - hardwareId.length(), 'A');

	if (padLen)
		hardwareId.append(padLen, '=');

	auto buf = base32.decode(hardwareId.c_str(), hardwareId.length(), &bufLen);

	BitStream3 bits;
	bits.Attach(buf.data(), 125);
	unsigned char version = 0;

	bits.Read(&version, 3);

	if ((version & 0x7) > 1)
		throw new LicensingException(STATUS_GENERIC_ERROR, "invalid hardware id version");

	list<string> propList;
	set<unsigned short> storedHashes;  // Store all hashes from license
	unsigned short hash1, hash2, hash3, hash4;

	// Use a points-based matching system for resilience
	int matchPoints = 0;
	const int REQUIRED_POINTS = 2;  // Need at least 2 components to match

	// Read UUID hash
	bits.ReadUInt16(&hash1);

	if (version == 0)
	{
		// Legacy version 0 support (memory size based)
		if (hash1 != h0)
		{
			unsigned long long memSize = 0;

#ifdef _WIN32
			wmi.GetPropertyList("SELECT Capacity FROM Win32_PhysicalMemory", "Capacity", &propList);
#else
			propList.push_back("4294967296");
#endif

			if (propList.size() > 0)
			{
				for (list<string>::iterator iter = propList.begin(); iter != propList.end(); iter++)
				{
					memSize += _atoi64(iter->c_str());
				}

				memSize = memSize / (1024 * 1024);

				if (hash1 != HashInt((int)memSize))
					return false;
			}
		}
		matchPoints += 2;  // UUID/Memory match gives 2 points
	}
	else
	{
		// Version 1+ - UUID based
#ifdef _WIN32
		wmi.GetPropertyList("SELECT * FROM Win32_ComputerSystemProduct", "UUID", &propList);
#endif
		if (propList.size() > 0)
		{
			string s = NormalizeHardwareString(propList.front());

			if ((s.length() > 1)
				&& (s != "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
				&& (s != "00000000000000000000000000000000"))
			{
				if (hash1 != h0 && hash1 == HashString(s.c_str()))
				{
					matchPoints += 2;  // UUID match is worth 2 points (most stable)
				}
			}
		}
	}

	// Read CPU hashes
	bits.ReadUInt16(&hash1);
	bits.ReadUInt16(&hash2);

	storedHashes.clear();
	if (hash1 != h0) storedHashes.insert(hash1);
	if (hash2 != h0) storedHashes.insert(hash2);

	propList.clear();

#ifdef _WIN32
	wmi.GetPropertyList("SELECT ProcessorId FROM Win32_Processor", "ProcessorId", &propList, 4);
#else
	GetCPUIDPropertyList(&propList);
#endif

	// Check if ANY current CPU matches stored hashes
	int cpuMatches = 0;
	for (auto& cpu : propList) {
		string s = NormalizeHardwareString(cpu);
		unsigned short currentHash = HashString(s.c_str());
		if (storedHashes.find(currentHash) != storedHashes.end()) {
			cpuMatches++;
		}
	}

	if (cpuMatches > 0) {
		matchPoints += 1;  // CPU match is worth 1 point
	}

	// Read MAC hashes (now 4 hashes for better resilience)
	bits.ReadUInt16(&hash1);
	bits.ReadUInt16(&hash2);
	bits.ReadUInt16(&hash3);
	bits.ReadUInt16(&hash4);

	storedHashes.clear();
	if (hash1 != h0) storedHashes.insert(hash1);
	if (hash2 != h0) storedHashes.insert(hash2);
	if (hash3 != h0) storedHashes.insert(hash3);
	if (hash4 != h0) storedHashes.insert(hash4);

	propList.clear();

#ifdef _WIN32
	wmi.GetPropertyList("SELECT MACAddress FROM Win32_NetworkAdapter WHERE (AdapterType = \"Ethernet 802.3\") AND (MACAddress IS NOT NULL) AND PNPDeviceId LIKE \"%PCI%\"", "MACAddress", &propList, 10);

	if (propList.size() == 0)
	{
		wmi.GetPropertyList("SELECT MACAddress FROM Win32_NetworkAdapter WHERE (AdapterType = \"Ethernet 802.3\") AND (MACAddress IS NOT NULL) AND PNPDeviceId LIKE \"%VMBUS%\"", "MACAddress", &propList, 10);
	}
#else
	GetMACPropertyList(&propList, 10);
#endif

	// Check if ANY current MAC address matches stored hashes
	int macMatches = 0;
	for (auto& mac : propList) {
		string s = NormalizeHardwareString(mac);
		unsigned short currentHash = HashString(s.c_str());
		if (storedHashes.find(currentHash) != storedHashes.end()) {
			macMatches++;
		}
	}

	if (macMatches > 0) {
		matchPoints += 1;  // MAC match is worth 1 point
	}

	// Read HDD hashes
	bits.ReadUInt16(&hash1);
	bits.ReadUInt16(&hash2);

	storedHashes.clear();
	if (hash1 != h0) storedHashes.insert(hash1);
	if (hash2 != h0) storedHashes.insert(hash2);

	propList.clear();

#ifdef _WIN32
	wmi.GetPropertyList("SELECT SerialNumber FROM Win32_PhysicalMedia WHERE SerialNumber IS NOT NULL", "SerialNumber", &propList, 10);
#else
	GetHDDPropertyList(&propList, 10);
#endif

	// Check if ANY current HDD serial matches stored hashes
	int hddMatches = 0;
	for (auto& serial : propList) {
		string s = NormalizeHardwareString(serial);
		unsigned short currentHash = HashString(s.c_str());
		if (storedHashes.find(currentHash) != storedHashes.end()) {
			hddMatches++;
		}
	}

	if (hddMatches > 0) {
		matchPoints += 1;  // HDD match is worth 1 point
	}

	// Validation passes if we have enough matching points
	// Typical valid scenarios:
	// - UUID (2) + CPU (1) = 3 points ✓
	// - UUID (2) + MAC (1) = 3 points ✓
	// - UUID (2) + HDD (1) = 3 points ✓
	// - CPU (1) + MAC (1) + HDD (1) = 3 points ✓ (if UUID changed but hardware same)
	return matchPoints >= REQUIRED_POINTS;
}