#pragma once

#include "ResolutionError.hpp"
#include "tl/expected.hpp"

#include <cstring>
#include <netdb.h>
#include <string>
#include <sys/socket.h>

namespace tw::net {

/**
 * Turns a host name or an address literal into a socket address.
 *
 * `family` is the family of the socket the result will be given to. AF_INET6
 * asks for IPv4-only names as ::ffff: mapped addresses, so that one dual-stack
 * socket reaches both; AF_UNSPEC takes the name as it comes and suits addresses
 * that are only being validated, displayed or stored.
 *
 * Blocks for the length of a DNS round trip when the name is not already known,
 * so it belongs at connect time rather than anywhere periodic.
 */
inline tl::expected<sockaddr_storage, ResolutionError>
resolve_host(const std::string& host, int port, sa_family_t family = AF_UNSPEC) {
    addrinfo hints {};
    hints.ai_family   = family;
    hints.ai_socktype = SOCK_DGRAM;

    // AI_ADDRCONFIG is deliberately absent. Together with AF_INET6 it discards
    // every result on a host that carries no global IPv6 address, which is the
    // default state of a container on a bridge network.
    if(family == AF_INET6) {
        hints.ai_flags = AI_V4MAPPED | AI_ALL;
    }

    // Passing the port as the service spares us setting sin_port or sin6_port
    // by hand once the family of the answer is known.
    const std::string service = std::to_string(port);

    addrinfo* results = nullptr;
    const int rc = ::getaddrinfo(host.c_str(), service.c_str(), &hints, &results);
    if(rc != 0) {
        return tl::make_unexpected(ResolutionError::from_gai(rc));
    }

    // The list arrives ordered by RFC 6724, so the head is the address the
    // system would have picked for itself.
    sockaddr_storage storage {};
    std::memcpy(&storage, results->ai_addr, results->ai_addrlen);

    ::freeaddrinfo(results);

    return storage;
}

}
