#include "Manager.hpp"

#include <algorithm>
#include <stdexcept>
#include <cstring>
#include <iostream>
#include <netdb.h>
#include <ostream>
#include <thread>
#include <random>
#include <sys/socket.h>

#include "behavior/events/audio/AudioEvent.hpp"
#include "behavior/tasks/server/ServerTask.hpp"
#include "utils/crypto/aes/AesKey.hpp"
#include "utils/crypto/rsa/RsaKeyPair.hpp"


namespace drp::managers {


Manager::Manager(const std::string& address, const std::string& port, const bool useIpv6) {
    std::cout << "Broadcast address: " << address << ":" << port << " (" << (useIpv6 ? "IPv6" : "IPv4") << ")" << std::endl;

    // hints for the communication
    addrinfo broadcastAddressHints {};
    broadcastAddressHints.ai_family = useIpv6 ? AF_INET6 : AF_INET;
    broadcastAddressHints.ai_socktype = SOCK_DGRAM;
    broadcastAddressHints.ai_protocol = IPPROTO_UDP;

    // create the client socket
    const int sock = socket(
        broadcastAddressHints.ai_family,
        broadcastAddressHints.ai_socktype,
        broadcastAddressHints.ai_protocol
    );
    if (sock < 0)
        throw std::runtime_error("Could not create the socket: " + std::string(strerror(errno)));

    // allow IPv6 multicast loopback so that we can receive our own messages.
    constexpr int socketLoopback = 1;
    if (setsockopt(
        sock,
        IPPROTO_IPV6,
        IPV6_MULTICAST_LOOP,
        &socketLoopback,
        sizeof(socketLoopback)
    ) < 0) {
        std::cerr << "Failed to set IPV6_MULTICAST_LOOP: " << strerror(errno) << std::endl;
    }

    // get the information for the broadcast local-link address
    addrinfo* broadcastAddressInfo = nullptr;
    if (const int error = getaddrinfo(
        address.c_str(),
        port.c_str(),
        &broadcastAddressHints,
        &broadcastAddressInfo
    ) != 0)
        throw std::runtime_error("Could not get the broadcast address: " + std::string(gai_strerror(error)));

    // hints for the bind address
    addrinfo anyAddressHints {};
    anyAddressHints.ai_family = useIpv6 ? AF_INET6 : AF_INET;
    anyAddressHints.ai_flags = AI_PASSIVE;
    anyAddressHints.ai_socktype = SOCK_DGRAM;
    anyAddressHints.ai_protocol = IPPROTO_UDP;

    // get the information for the broadcast local-link address
    addrinfo *anyAddressInfo;
    if (const int error = getaddrinfo(
        nullptr,
        port.c_str(),
        &anyAddressHints,
        &anyAddressInfo
    ) != 0)
        throw std::runtime_error("Could not get the any address: " + std::string(gai_strerror(error)));

    // bind the socket to the address
    if (bind(
        sock,
        anyAddressInfo->ai_addr,
        anyAddressInfo->ai_addrlen
    ) < 0)
        throw std::runtime_error("Could not bind to the address: " + std::string(strerror(errno)));

    // create the context
    this->context = std::make_shared<Context>(sock, broadcastAddressInfo);
    // TODO(Faraphel): should only be enabled if it can really emit sound.
    this->context->me.serverEnabled = true;

    // create the sub-managers.
    this->sendManager = std::make_unique<SendManager>(context);
    this->receiveManager = std::make_unique<ReceiveManager>(context);
}

Manager::~Manager() {
    freeaddrinfo(this->context->broadcastAddressInfo);
}


void Manager::loop() {
    // run an event receiver and sender
    std::thread senderThread(&SendManager::loop, this->sendManager.get());
    std::thread receiverThread(&ReceiveManager::loop, this->receiveManager.get());

    senderThread.join();
    receiverThread.join();
}


}