/*
    * Dns.cpp
    * DNS resolver (kernel-level, RFC 1035)
    * Copyright (c) 2025-2026 Daniel Hammer
*/

#include "Dns.hpp"
#include <Net/Udp.hpp>
#include <Net/ByteOrder.hpp>
#include <Net/NetConfig.hpp>
#include <Libraries/Memory.hpp>
#include <Libraries/String.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Sched/Scheduler.hpp>
#include <Terminal/Terminal.hpp>

namespace Net::Dns {

    // ---- DNS packet constants ----

    static constexpr uint16_t DNS_PORT       = 53;
    static constexpr uint16_t DNS_FLAGS_RD   = 0x0100; // Recursion Desired
    static constexpr uint16_t DNS_QTYPE_A    = 1;
    static constexpr uint16_t DNS_QCLASS_IN  = 1;

    // ---- Simple cache ----

    static constexpr int CACHE_SIZE = 8;

    struct CacheEntry {
        char     hostname[128];
        uint32_t ip;
        uint32_t ttl;         // TTL in seconds
        uint64_t timestamp;   // ms when cached
        bool     valid;
    };

    static CacheEntry g_cache[CACHE_SIZE] = {};

    static bool streq(const char* a, const char* b) {
        while (*a && *b) {
            if (*a != *b) return false;
            a++; b++;
        }
        return *a == *b;
    }

    static uint32_t CacheLookup(const char* hostname) {
        uint64_t now = Timekeeping::GetMilliseconds();
        for (int i = 0; i < CACHE_SIZE; i++) {
            if (!g_cache[i].valid) continue;
            if (!streq(g_cache[i].hostname, hostname)) continue;
            // Check TTL
            uint64_t elapsed = (now - g_cache[i].timestamp) / 1000;
            if (elapsed < g_cache[i].ttl) {
                return g_cache[i].ip;
            }
            // Expired
            g_cache[i].valid = false;
            return 0;
        }
        return 0;
    }

    static void CacheStore(const char* hostname, uint32_t ip, uint32_t ttl) {
        if (ttl == 0) ttl = 60; // Minimum 60s TTL

        // Find free or oldest slot
        int slot = 0;
        uint64_t oldestTime = ~0ULL;
        for (int i = 0; i < CACHE_SIZE; i++) {
            if (!g_cache[i].valid) { slot = i; break; }
            if (g_cache[i].timestamp < oldestTime) {
                oldestTime = g_cache[i].timestamp;
                slot = i;
            }
        }

        CacheEntry& e = g_cache[slot];
        // Copy hostname
        int len = 0;
        while (hostname[len] && len < 126) { e.hostname[len] = hostname[len]; len++; }
        e.hostname[len] = '\0';
        e.ip = ip;
        e.ttl = ttl;
        e.timestamp = Timekeeping::GetMilliseconds();
        e.valid = true;
    }

    // ---- DNS query building ----

    // Encode a hostname as DNS labels: "example.com" -> "\x07example\x03com\x00"
    // Returns number of bytes written, or 0 on error.
    static int EncodeName(const char* hostname, uint8_t* out, int maxLen) {
        int outPos = 0;
        const char* p = hostname;

        while (*p) {
            // Find the next dot or end
            const char* dot = p;
            while (*dot && *dot != '.') dot++;
            int labelLen = (int)(dot - p);

            if (labelLen == 0 || labelLen > 63) return 0;
            if (outPos + 1 + labelLen >= maxLen) return 0;

            out[outPos++] = (uint8_t)labelLen;
            for (int i = 0; i < labelLen; i++) {
                out[outPos++] = (uint8_t)p[i];
            }

            p = dot;
            if (*p == '.') p++;
        }

        if (outPos >= maxLen) return 0;
        out[outPos++] = 0; // Root label terminator
        return outPos;
    }

    // Build a DNS query packet. Returns total packet length, or 0 on error.
    static int BuildQuery(uint16_t id, const char* hostname, uint8_t* packet, int maxLen) {
        if (maxLen < 12) return 0;

        // Header (12 bytes)
        packet[0]  = (uint8_t)(id >> 8);
        packet[1]  = (uint8_t)(id & 0xFF);
        packet[2]  = (uint8_t)(DNS_FLAGS_RD >> 8);   // Flags high: RD=1
        packet[3]  = (uint8_t)(DNS_FLAGS_RD & 0xFF);  // Flags low
        packet[4]  = 0; packet[5]  = 1;  // QDCOUNT = 1
        packet[6]  = 0; packet[7]  = 0;  // ANCOUNT = 0
        packet[8]  = 0; packet[9]  = 0;  // NSCOUNT = 0
        packet[10] = 0; packet[11] = 0;  // ARCOUNT = 0

        // Question section
        int nameLen = EncodeName(hostname, packet + 12, maxLen - 12 - 4);
        if (nameLen == 0) return 0;

        int pos = 12 + nameLen;
        if (pos + 4 > maxLen) return 0;

        // QTYPE = A (1)
        packet[pos++] = 0;
        packet[pos++] = DNS_QTYPE_A;
        // QCLASS = IN (1)
        packet[pos++] = 0;
        packet[pos++] = DNS_QCLASS_IN;

        return pos;
    }

    // ---- DNS response parsing ----

    // Skip over a DNS name in the packet (handles compression pointers).
    // Returns the new offset, or -1 on error.
    static int SkipName(const uint8_t* packet, int packetLen, int offset) {
        int maxJumps = 32; // prevent infinite loops
        bool jumped = false;
        int returnOffset = -1;

        while (offset < packetLen && maxJumps > 0) {
            uint8_t len = packet[offset];

            if (len == 0) {
                // End of name
                offset++;
                return jumped ? returnOffset : offset;
            }

            if ((len & 0xC0) == 0xC0) {
                // Compression pointer
                if (offset + 1 >= packetLen) return -1;
                if (!jumped) returnOffset = offset + 2;
                offset = ((len & 0x3F) << 8) | packet[offset + 1];
                jumped = true;
                maxJumps--;
                continue;
            }

            // Regular label
            offset += 1 + len;
            maxJumps--;
        }

        return -1;
    }

    struct DnsAnswer {
        uint32_t ip;
        uint32_t ttl;
        bool     found;
    };

    // Parse a DNS response and extract the first A record.
    static DnsAnswer ParseResponse(uint16_t expectedId, const uint8_t* packet, int packetLen) {
        DnsAnswer result = {0, 0, false};

        if (packetLen < 12) return result;

        // Check ID
        uint16_t id = ((uint16_t)packet[0] << 8) | packet[1];
        if (id != expectedId) return result;

        // Check QR bit (must be response)
        if (!(packet[2] & 0x80)) return result;

        // Check RCODE (must be 0 = no error)
        uint8_t rcode = packet[3] & 0x0F;
        if (rcode != 0) return result;

        uint16_t qdcount = ((uint16_t)packet[4] << 8) | packet[5];
        uint16_t ancount = ((uint16_t)packet[6] << 8) | packet[7];

        // Skip question section
        int offset = 12;
        for (uint16_t i = 0; i < qdcount; i++) {
            offset = SkipName(packet, packetLen, offset);
            if (offset < 0) return result;
            offset += 4; // QTYPE + QCLASS
            if (offset > packetLen) return result;
        }

        // Parse answers
        for (uint16_t i = 0; i < ancount; i++) {
            offset = SkipName(packet, packetLen, offset);
            if (offset < 0 || offset + 10 > packetLen) return result;

            uint16_t atype  = ((uint16_t)packet[offset] << 8) | packet[offset + 1];
            // uint16_t aclass = ((uint16_t)packet[offset + 2] << 8) | packet[offset + 3];
            uint32_t attl   = ((uint32_t)packet[offset + 4] << 24) |
                              ((uint32_t)packet[offset + 5] << 16) |
                              ((uint32_t)packet[offset + 6] << 8)  |
                              ((uint32_t)packet[offset + 7]);
            uint16_t rdlen  = ((uint16_t)packet[offset + 8] << 8) | packet[offset + 9];
            offset += 10;

            if (offset + rdlen > packetLen) return result;

            if (atype == DNS_QTYPE_A && rdlen == 4) {
                // A record: 4-byte IPv4 address (already in network byte order)
                result.ip = ((uint32_t)packet[offset])
                          | ((uint32_t)packet[offset + 1] << 8)
                          | ((uint32_t)packet[offset + 2] << 16)
                          | ((uint32_t)packet[offset + 3] << 24);
                result.ttl = attl;
                result.found = true;
                return result;
            }

            offset += rdlen;
        }

        return result;
    }

    // ---- Resolve state (shared with UDP callback) ----

    static volatile bool g_gotResponse = false;
    static volatile uint16_t g_currentId = 0;
    static uint8_t g_responseBuffer[512];
    static volatile int g_responseLen = 0;

    static void DnsRecvCallback(uint32_t srcIp, uint16_t srcPort,
                                 uint16_t dstPort,
                                 const uint8_t* data, uint16_t length) {
        (void)srcIp;
        (void)srcPort;
        (void)dstPort;

        if (g_gotResponse) return; // Already got a response
        if (length > sizeof(g_responseBuffer)) length = sizeof(g_responseBuffer);

        memcpy(g_responseBuffer, data, length);
        g_responseLen = length;
        g_gotResponse = true;
    }

    // ---- Simple PRNG for transaction IDs ----

    static uint16_t g_nextId = 0x4E53; // "NS"

    static uint16_t NextId() {
        g_nextId = g_nextId * 25173 + 13849;
        return g_nextId;
    }

    // ---- Check if string is already an IP address ----

    static bool IsIpAddress(const char* s) {
        int dotCount = 0;
        bool hasDigit = false;
        for (int i = 0; s[i]; i++) {
            if (s[i] >= '0' && s[i] <= '9') {
                hasDigit = true;
            } else if (s[i] == '.') {
                if (!hasDigit) return false;
                dotCount++;
                hasDigit = false;
            } else {
                return false;
            }
        }
        return hasDigit && dotCount == 3;
    }

    // ---- Public API ----

    uint32_t Resolve(const char* hostname, uint32_t timeoutMs) {
        if (hostname == nullptr || hostname[0] == '\0') return 0;

        // Don't try to resolve IP addresses
        if (IsIpAddress(hostname)) return 0;

        // Check cache first
        uint32_t cached = CacheLookup(hostname);
        if (cached != 0) return cached;

        // Check DNS server is configured
        uint32_t dnsServer = Net::GetDnsServer();
        if (dnsServer == 0) return 0;

        // Pick a local port for receiving the response (ephemeral range)
        uint16_t localPort = 10000 + (NextId() % 50000);
        uint16_t txId = NextId();

        // Build DNS query
        uint8_t queryPacket[512];
        int queryLen = BuildQuery(txId, hostname, queryPacket, sizeof(queryPacket));
        if (queryLen == 0) return 0;

        // Reset response state
        g_gotResponse = false;
        g_responseLen = 0;
        g_currentId = txId;

        // Bind our receive port
        if (!Net::Udp::Bind(localPort, DnsRecvCallback)) {
            // Port might be in use, try another
            localPort = 10000 + (NextId() % 50000);
            if (!Net::Udp::Bind(localPort, DnsRecvCallback)) {
                return 0;
            }
        }

        // Send the query to DNS server port 53
        bool sent = Net::Udp::Send(dnsServer, localPort, DNS_PORT, queryPacket, (uint16_t)queryLen);
        if (!sent) {
            Net::Udp::Unbind(localPort);
            return 0;
        }

        // Wait for response with timeout
        uint64_t start = Timekeeping::GetMilliseconds();
        while (!g_gotResponse) {
            if (Timekeeping::GetMilliseconds() - start >= timeoutMs) {
                Net::Udp::Unbind(localPort);
                return 0;
            }
            Sched::Schedule();
        }

        // Unbind the port
        Net::Udp::Unbind(localPort);

        // Parse the response
        DnsAnswer answer = ParseResponse(txId, g_responseBuffer, g_responseLen);
        if (!answer.found) return 0;

        // Cache the result
        CacheStore(hostname, answer.ip, answer.ttl);

        return answer.ip;
    }

}
