#include <ws2tcpip.h>
#include <iphlpapi.h>

#include "common/netif.h"
#include "common/mallocHelper.h"
#include "common/windows/unicode.h"
#include "common/debug.h"
#include "localip.h"

#define FF_LOCALIP_NIFLAG(name) { IP_ADAPTER_##name, #name }

static const FFLocalIpNIFlag niFlagOptions[] = {
    FF_LOCALIP_NIFLAG(DDNS_ENABLED),
    FF_LOCALIP_NIFLAG(REGISTER_ADAPTER_SUFFIX),
    FF_LOCALIP_NIFLAG(DHCP_ENABLED),
    FF_LOCALIP_NIFLAG(RECEIVE_ONLY),
    FF_LOCALIP_NIFLAG(NO_MULTICAST),
    FF_LOCALIP_NIFLAG(IPV6_OTHER_STATEFUL_CONFIG),
    FF_LOCALIP_NIFLAG(NETBIOS_OVER_TCPIP_ENABLED),
    FF_LOCALIP_NIFLAG(IPV4_ENABLED),
    FF_LOCALIP_NIFLAG(IPV6_ENABLED),
    FF_LOCALIP_NIFLAG(IPV6_MANAGE_ADDRESS_CONFIG),
    // sentinel
    {},
};

const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) {
    FF_DEBUG("Starting local IP detection with showType=0x%X, namePrefix='%.*s'",
        options->showType,
        (int) options->namePrefix.length,
        options->namePrefix.chars);

    IP_ADAPTER_ADDRESSES* FF_AUTO_FREE adapter_addresses = NULL;

    // Multiple attempts in case interfaces change while
    // we are in the middle of querying them.
    DWORD adapter_addresses_buffer_size = 0;
    for (int attempts = 0;; ++attempts) {
        FF_DEBUG("Attempt %d to get adapter addresses, buffer size: %lu", attempts + 1, adapter_addresses_buffer_size);

        if (adapter_addresses_buffer_size) {
            adapter_addresses = (IP_ADAPTER_ADDRESSES*) realloc(adapter_addresses, adapter_addresses_buffer_size);
            assert(adapter_addresses);
        }

        DWORD family = options->showType & FF_LOCALIP_TYPE_IPV4_BIT
            ? options->showType & FF_LOCALIP_TYPE_IPV6_BIT ? AF_UNSPEC : AF_INET
            : AF_INET6;
        FF_DEBUG("Calling GetAdaptersAddresses with family=%u, flags=0x%X", (unsigned) family, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER);

        DWORD error = GetAdaptersAddresses(
            family,
            GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
            NULL,
            adapter_addresses,
            &adapter_addresses_buffer_size);

        if (error == ERROR_SUCCESS) {
            FF_DEBUG("GetAdaptersAddresses succeeded on attempt %d", attempts + 1);
            break;
        } else if (ERROR_BUFFER_OVERFLOW == error && attempts < 4) {
            FF_DEBUG("Buffer overflow, need %lu bytes, retrying", adapter_addresses_buffer_size);
            continue;
        } else {
            FF_DEBUG("GetAdaptersAddresses failed with error %lu after %d attempts", error, attempts + 1);
            return "GetAdaptersAddresses() failed";
        }
    }

    FF_A_UNUSED int adapterCount = 0, processedCount = 0;

    // Iterate through all of the adapters
    for (IP_ADAPTER_ADDRESSES* adapter = adapter_addresses; adapter; adapter = adapter->Next) {
        adapterCount++;

        FF_DEBUG("Processing adapter %d: IfIndex=%u, IfType=%u, OperStatus=%u",
            adapterCount,
            (unsigned) adapter->IfIndex,
            (unsigned) adapter->IfType,
            (unsigned) adapter->OperStatus);

        if (adapter->OperStatus != IfOperStatusUp) {
            FF_DEBUG("Skipping adapter %u (not operational, status=%d)", (unsigned) adapter->IfIndex, adapter->OperStatus);
            continue;
        }

        bool isLoop = adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK;
        FF_DEBUG("Adapter %u: isLoopback=%s", (unsigned) adapter->IfIndex, isLoop ? "true" : "false");

        if (isLoop && !(options->showType & FF_LOCALIP_TYPE_LOOP_BIT)) {
            FF_DEBUG("Skipping loopback adapter %u (loopback not requested)", (unsigned) adapter->IfIndex);
            continue;
        }

        FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(adapter->FriendlyName);
        FF_DEBUG("Adapter %u name: '%s'", (unsigned) adapter->IfIndex, name.chars);

        if (options->namePrefix.length && !ffStrbufStartsWith(&name, &options->namePrefix)) {
            FF_DEBUG("Skipping adapter %u (name doesn't match prefix '%.*s')",
                (unsigned) adapter->IfIndex,
                (int) options->namePrefix.length,
                options->namePrefix.chars);
            continue;
        }

        if (options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT) {
            if (!((options->showType & FF_LOCALIP_TYPE_IPV4_BIT) && ffNetifGetDefaultRouteV4()->ifIndex == adapter->IfIndex) &&
                !((options->showType & FF_LOCALIP_TYPE_IPV6_BIT) && ffNetifGetDefaultRouteV6()->ifIndex == adapter->IfIndex)) {
                FF_DEBUG("Skipping interface %u (not default route interface)", (unsigned) adapter->IfIndex);
                continue;
            }
        }

        processedCount++;
        FF_DEBUG("Creating result item for adapter %u ('%s')", (unsigned) adapter->IfIndex, name.chars);

        FFLocalIpResult* item = FF_LIST_ADD(FFLocalIpResult, *results);
        ffStrbufInitMove(&item->name, &name);
        ffStrbufInit(&item->ipv4);
        ffStrbufInit(&item->ipv6);
        ffStrbufInit(&item->mac);
        ffStrbufInit(&item->flags);
        item->defaultRoute = FF_LOCALIP_TYPE_NONE;
        item->speed = -1;
        item->mtu = -1;

        uint32_t typesToAdd = options->showType & (FF_LOCALIP_TYPE_IPV4_BIT | FF_LOCALIP_TYPE_IPV6_BIT | FF_LOCALIP_TYPE_ALL_IPS_BIT);
        FF_DEBUG("Types to add for adapter %u: 0x%X", (unsigned) adapter->IfIndex, typesToAdd);

        FF_A_UNUSED int ipv4Count = 0, ipv6Count = 0;

        for (IP_ADAPTER_UNICAST_ADDRESS* ifa = adapter->FirstUnicastAddress; ifa; ifa = ifa->Next) {
            FF_DEBUG("Processing unicast address: prefix origin=%d, suffix origin=%d, family=%d, DadState=%d",
                ifa->PrefixOrigin,
                ifa->SuffixOrigin,
                ifa->Address.lpSockaddr->sa_family,
                ifa->DadState);

            if (!(options->showType & FF_LOCALIP_TYPE_ALL_IPS_BIT)) {
                if (ifa->DadState != IpDadStatePreferred) {
                    FF_DEBUG("Skipping address (not preferred)");
                    continue;
                }

                if (ifa->SuffixOrigin == IpSuffixOriginRandom) {
                    FF_DEBUG("Skipping temporary address (random suffix)");
                    continue;
                }

                // MIB_UNICASTIPADDRESS_ROW::SkipAsSource
            }

            if (ifa->Address.lpSockaddr->sa_family == AF_INET) {
                if (!(typesToAdd & (FF_LOCALIP_TYPE_IPV4_BIT | FF_LOCALIP_TYPE_ALL_IPS_BIT))) {
                    FF_DEBUG("Skipping IPv4 address (not requested in typesToAdd=0x%X)", typesToAdd);
                    continue;
                }

                bool isDefaultRoute = ((options->showType & FF_LOCALIP_TYPE_IPV4_BIT) && ffNetifGetDefaultRouteV4()->ifIndex == adapter->IfIndex);
                if ((options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT) && !isDefaultRoute) {
                    FF_DEBUG("Skipping IPv4 address (not on default route interface)");
                    continue;
                }

                SOCKADDR_IN* ipv4 = (SOCKADDR_IN*) ifa->Address.lpSockaddr;
                char addressBuffer[INET_ADDRSTRLEN + 10];
                char* end = RtlIpv4AddressToStringA(&ipv4->sin_addr, addressBuffer);

                if ((options->showType & FF_LOCALIP_TYPE_PREFIX_LEN_BIT) && ifa->OnLinkPrefixLength) {
                    end += snprintf(end, 10, "/%u", (unsigned) ifa->OnLinkPrefixLength);
                }

                FF_DEBUG("Adding IPv4 address: %s (isDefaultRoute=%s)", addressBuffer, isDefaultRoute ? "true" : "false");

                if (item->ipv4.length) {
                    ffStrbufAppendC(&item->ipv4, ',');
                }
                ffStrbufAppendNS(&item->ipv4, (uint32_t) (end - addressBuffer), addressBuffer);
                if (isDefaultRoute) {
                    item->defaultRoute |= FF_LOCALIP_TYPE_IPV4_BIT;
                }

                ipv4Count++;
                typesToAdd &= ~(unsigned) FF_LOCALIP_TYPE_IPV4_BIT;
                if (typesToAdd == 0) {
                    break;
                }
            } else if (ifa->Address.lpSockaddr->sa_family == AF_INET6) {
                if (!(typesToAdd & (FF_LOCALIP_TYPE_IPV6_BIT | FF_LOCALIP_TYPE_ALL_IPS_BIT))) {
                    FF_DEBUG("Skipping IPv6 address (not requested in typesToAdd=0x%X)", typesToAdd);
                    continue;
                }

                SOCKADDR_IN6* ipv6 = (SOCKADDR_IN6*) ifa->Address.lpSockaddr;

                FFLocalIpIpv6Type ipv6Type = FF_LOCALIP_IPV6_TYPE_NONE;
                if (IN6_IS_ADDR_GLOBAL(&ipv6->sin6_addr)) {
                    ipv6Type |= FF_LOCALIP_IPV6_TYPE_GUA_BIT;
                } else if (IN6_IS_ADDR_UNIQUE_LOCAL(&ipv6->sin6_addr)) {
                    ipv6Type |= FF_LOCALIP_IPV6_TYPE_ULA_BIT;
                } else if (IN6_IS_ADDR_LINKLOCAL(&ipv6->sin6_addr)) {
                    ipv6Type |= FF_LOCALIP_IPV6_TYPE_LLA_BIT;
                } else {
                    ipv6Type |= FF_LOCALIP_IPV6_TYPE_UNKNOWN_BIT;
                }

                if (!(options->ipv6Type & ipv6Type)) {
                    FF_DEBUG("Skipping IPv6 address (doesn't match requested type 0x%X)", options->ipv6Type);
                    continue;
                }

                bool isDefaultRoute = ((options->showType & FF_LOCALIP_TYPE_IPV6_BIT) && ffNetifGetDefaultRouteV6()->ifIndex == adapter->IfIndex);
                if ((options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT) && !isDefaultRoute) {
                    FF_DEBUG("Skipping IPv6 address (not on default route interface)");
                    continue;
                }

                char addressBuffer[INET6_ADDRSTRLEN + 10];
                char* end = RtlIpv6AddressToStringA(&ipv6->sin6_addr, addressBuffer);

                if ((options->showType & FF_LOCALIP_TYPE_PREFIX_LEN_BIT) && ifa->OnLinkPrefixLength) {
                    end += snprintf(end, 10, "/%u", (unsigned) ifa->OnLinkPrefixLength);
                }

                FF_DEBUG("Adding IPv6 address: %s (isDefaultRoute=%s)", addressBuffer, isDefaultRoute ? "true" : "false");

                if (item->ipv6.length) {
                    ffStrbufAppendC(&item->ipv6, ',');
                }
                ffStrbufAppendNS(&item->ipv6, (uint32_t) (end - addressBuffer), addressBuffer);
                if (isDefaultRoute) {
                    item->defaultRoute |= FF_LOCALIP_TYPE_IPV6_BIT;
                }

                ipv6Count++;
                typesToAdd &= ~(unsigned) FF_LOCALIP_TYPE_IPV6_BIT;
                if (typesToAdd == 0) {
                    break;
                }
            }
        }

        FF_DEBUG("Adapter %u: collected %d IPv4 and %d IPv6 addresses", (unsigned) adapter->IfIndex, ipv4Count, ipv6Count);

        if (options->showType & FF_LOCALIP_TYPE_SPEED_BIT) {
            item->speed = (int32_t) (adapter->ReceiveLinkSpeed / 1000000);
            FF_DEBUG("Adapter %u speed: %d Mbps (raw: %llu)", (unsigned) adapter->IfIndex, item->speed, adapter->ReceiveLinkSpeed);
        }
        if (options->showType & FF_LOCALIP_TYPE_MTU_BIT) {
            item->mtu = (int32_t) adapter->Mtu;
            FF_DEBUG("Adapter %u MTU: %d", (unsigned) adapter->IfIndex, item->mtu);
        }
        if (options->showType & FF_LOCALIP_TYPE_FLAGS_BIT) {
            ffLocalIpFillNIFlags(&item->flags, adapter->Flags, niFlagOptions);
            FF_DEBUG("Adapter %u flags: 0x%lX -> '%s'", (unsigned) adapter->IfIndex, adapter->Flags, item->flags.chars);
        }
        if (options->showType & FF_LOCALIP_TYPE_MAC_BIT && adapter->PhysicalAddressLength == 6) {
            uint8_t* ptr = adapter->PhysicalAddress;
            ffStrbufSetF(&item->mac, "%02x:%02x:%02x:%02x:%02x:%02x", ptr[0], ptr[1], ptr[2], ptr[3], ptr[4], ptr[5]);
            FF_DEBUG("Adapter %u MAC: %s", (unsigned) adapter->IfIndex, item->mac.chars);
        }
    }

    FF_DEBUG("Local IP detection completed: scanned %d adapters, processed %d, results count: %u",
        adapterCount,
        processedCount,
        results->length);

    return NULL;
}
