#include "Client.hpp"
#include "Socket.hpp"
#include "Sender.hpp"
#include "Receiver.hpp"

#include <cerrno>
#ifndef PLATFORM_WINDOWS
#   include <sys/types.h>
#   include <sys/socket.h>
#   include <netinet/in.h>
#   include <arpa/inet.h>
#   include <netdb.h>
#else
#   include <ws2tcpip.h>
#endif



void Client::fetchAddr(const std::string& addr, unsigned port) {
#   ifdef HAS_ADDRINFO
    // Set up hints
    addrinfo hints;
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_INET; //TODO: Care about IPv6
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = AI_CANONNAME;

    // Get port as C string
    char portStr[7];
    sprintf(portStr, "%u", port);

    // Get addrinfo
    auto error = getaddrinfo(addr.c_str(), portStr, &hints, &addrInfo);
    auto bad = addrInfo == nullptr;
#   else
    addrInfo = gethostbyname(addr.c_str());
    auto error = errno;
    auto bad = addrInfo == nullptr || addrInfo->h_addr_list[0] == nullptr;
#   endif

    // Check for error
    if (bad) {
        throw Exception("DNS failed to look up hostname: "+std::string(strerror(error))+" ("+addr+')');
    }
}

Client::Client(const std::string& addr, unsigned port) {
    // Create socket
    connection = std::make_unique<SocketConnection<Sender::Simple, Receiver::Simple>>(Socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)); //TODO: Care about IPv6
    if (*connection < 0) [[unlikely]] {
        throw Exception("Failed to create TCP socket");
    }

    // Fetch address
    fetchAddr(addr, port);

#   ifdef HAS_ADDRINFO
    // Connect to server
    if (connect(*connection, addrInfo->ai_addr, addrInfo->ai_addrlen) != 0) [[unlikely]] {
        throw Exception("Connection for HTTP::Request has been declined");
    }
#   else
    // Connect to server
    struct sockaddr_in sain;
    sain.sin_family = AF_INET;
    sain.sin_port = htons(port);
    sain.sin_addr.s_addr = *reinterpret_cast<unsigned long *>(addrInfo->h_addr_list[0]);
    if (connect(*connection, reinterpret_cast<sockaddr *>(&sain), sizeof(sain)) != 0) [[unlikely]] {
        throw Exception("Connection has been declined");
    }
#   endif
}

void Client::ask(std::string_view prompt, const std::function<void (unsigned progress)>& on_progress, const std::function<void (std::string_view token)>& on_token) {
    std::string fres;

    // Send prompt length
    uint8_t len = prompt.length();
    connection->writeObject(len, true);

    // Send prompt
    connection->write(prompt);

    // Receive progress
    for (;;) {
        uint8_t progress;

        // Receive percentage
        connection->readObject(progress);

        // Run on_progress callback
        on_progress(progress);

        // Stop at 100%
        if (progress == 100) break;
    }

    // Receive response
    for (;;) {
        // Receive response length
        connection->readObject(len);

        // End if zero
        if (len == 0xFF) break;

        // Receive response
        const auto token = connection->read(len);

        // Run on_token callback
        on_token(token);
    }
}