#include <Settings/Settings.hpp>
#include <filesystem>
#include <thread>
#include <signal.h>
#include <Server/Server.hpp>
#include <Log/Log.hpp>
#include <World/LocalWorld/ServerWorld/ServerWorld.hpp>
#include <Utilities/Socket.hpp>
#include <Utilities/Utilities.hpp>
#include <Protocol/Protocol.hpp>
#include <CommandQueue/CommandQueue.hpp>
#include <Messenger/Messenger.hpp>
#include <UserDB/UserDB.hpp>
#include <Utilities/ServerInit.hpp>
#include <ChunkHandle/ChunkHandle.hpp>
#include <ChunkWindow/ChunkWindow.hpp>

struct AuthResult
{
    bool isSuccess;
    Protocol::ServerGreetingCode respcode;
    std::string username;
    UserDB::SessionID sid;
    UserDB::PermissionSet perms;

    static inline AuthResult success(std::string username, UserDB::SessionID sid, UserDB::PermissionSet perms)
    {
        AuthResult result;
        result.isSuccess = true;
        result.username = username;
        result.sid = sid;
        result.perms = perms;
        return result;
    }

    static inline AuthResult fail(Protocol::ServerGreetingCode respcode)
    {
        AuthResult result;
        result.isSuccess = false;
        result.respcode = respcode;
        return result;
    }
};

CommandQueue<ServerWorld> serverCommandQueue;

extern volatile bool running;

static void onSigInt(int ignore)
{
    (void)ignore;
    running = false;
}

static void respondToPing(SOCKET clientSocket)
{
    Protocol::ServerGreetingHeader greetingHeader(Protocol::ServerGreetingCode::Pong);
    if (!writeSocket(clientSocket, (const char*) &greetingHeader, sizeof(greetingHeader)))
    {
        return;
    }

    char motto[32] = {};
    memcpy(motto, settings.motto.data(), std::min(settings.motto.size(), (size_t) 32));
    writeSocket(clientSocket, motto, 32);
}

static void receivingThread(
    std::string username,
    std::shared_ptr<Player> player
)
{
    auto messenger = player->client;

    bool normalShutdown = messenger->processIncoming(
        [messenger, player, username](
            Protocol::MessageType messageType,
            const std::vector<uint8_t> &messagePayload
        )
        {
            if (player->isBeingForcedOut)
            {
                return false;
            }

            if (messageType == Protocol::MessageType::Logout)
            {
                Log::info(formatString("User '$+' logged out.", username));
                return false;
            }

            std::vector<uint8_t> payload = messagePayload;
            serverCommandQueue.add([player, messenger, messageType, payload](ServerWorld &world)
            {
                if (messageType == Protocol::MessageType::KeepAlive)
                {
                    deserialize(payload.data(), payload.size(), player->isTyping);

                    std::vector<uint8_t> payload;
                    std::vector<std::string> playerNames = world.getPlayerNames();
                    std::vector<std::string> typingPlayerNames = world.getTypingPlayerNames(player->getName());
                    uint64_t worldTime = world.getWorldTime();
                    serializeInto(payload, (uint32_t) world.getTimeOfDay(), playerNames, typingPlayerNames, worldTime);

                    messenger->sendRaw(Protocol::MessageType::KeepAlive, payload.data(), payload.size());
                }
                else if (messageType == Protocol::MessageType::EntityMessage)
                {
                    if (payload.size() < sizeof(Protocol::EntityMessage))
                    {
                        messenger->sendProtocolError("EntityMessage too short");
                    }
                    else
                    {
                        const auto *entityMessage = reinterpret_cast<const Protocol::EntityMessage*>(payload.data());
                        world.deliverEntityMessage(
                            entityMessage->entityID, entityMessage->payload,
                            payload.size() - sizeof(Protocol::EntityMessage)
                        );
                    }
                }
                else if (messageType == Protocol::MessageType::RemoteCall)
                {
                    uint8_t channelId;
                    uint8_t opcode;
                    uint16_t stamp;
                    uint64_t promiseId;
                    NestedPayload rpcPayload;

                    if (deserialize(
                        payload.data(), payload.size(),
                        channelId,
                        opcode,
                        stamp,
                        promiseId,
                        rpcPayload
                    ) != payload.size())
                    {
                        messenger->sendProtocolError("Received a malformed RemoteCall message.");
                        return;
                    }

                    auto *channel = player->getControllerChannel(channelId);
                    if (channel == nullptr)
                    {
                        messenger->sendProtocolError(formatString("RemoteCall: Invalid controller channel ID: $+", (int) channelId));
                        return;
                    }

                    channel->dispatch(opcode, stamp, promiseId, rpcPayload);
                }
                else if (messageType == Protocol::MessageType::RemoteReturn)
                {
                    uint8_t channelId;
                    uint16_t stamp;
                    uint64_t promiseId;
                    NestedPayload rpcPayload;

                    if (deserialize(
                        payload.data(), payload.size(),
                        channelId,
                        stamp,
                        promiseId,
                        rpcPayload
                    ) != payload.size())
                    {
                        messenger->sendProtocolError("Received a malformed RemoteReturn message.");
                        return;
                    }

                    auto *channel = player->getControllerChannel(channelId);
                    if (channel == nullptr)
                    {
                        messenger->sendProtocolError(formatString("RemoteReturn: Invalid controller channel ID: $+", (int) channelId));
                        return;
                    }

                    channel->resolve(stamp, promiseId, rpcPayload);
                }
                else if (messageType == Protocol::MessageType::GuiClosed)
                {
                    uint16_t stamp;

                    if (deserialize(payload.data(), payload.size(), stamp) != payload.size())
                    {
                        messenger->sendProtocolError(formatString("Received a malformed GuiClosed message."));
                        return;
                    }

                    auto *channel = player->getControllerChannel(0);
                    channel->onClosedByClient(stamp);
                }
                else if (messageType == Protocol::MessageType::SetClientPrefs)
                {
                    if (payload.size() != sizeof(Protocol::ClientPrefs))
                    {
                        messenger->sendProtocolError("Received a malformed SetClientPrefs message.");
                        return;
                    }

                    std::memcpy(&player->clientPrefs, payload.data(), payload.size());
                }
                else
                {
                    messenger->sendProtocolError(formatString("Unrecognised message type: '$+'",
                        static_cast<uint32_t>(messageType)));
                }
            });

            return true;
        }
    );

    if (!normalShutdown)
    {
        Log::error(formatString("User '$+' has been disconnected.", username));
    }

    serverCommandQueue.add([messenger, username](ServerWorld &world)
    {
        world.removePlayer(username);
    });
}

static AuthResult handleSessionAuth(SOCKET clientSocket, Protocol::LoginHeader &loginHeader)
{
    if (loginHeader.payloadSize <= sizeof(UserDB::SessionID))
    {
        return AuthResult::fail(Protocol::ServerGreetingCode::LoginFailed);
    }

    UserDB::SessionID sid;
    if (!readSocket(clientSocket, (char*) &sid, sizeof(sid)))
    {
        return AuthResult::fail(Protocol::ServerGreetingCode::LoginFailed);
    }

    size_t usernameLen = loginHeader.payloadSize-sizeof(sid);

    char *userString = new char[usernameLen+1];
    if (!readSocket(clientSocket, userString, usernameLen))
    {
        delete[] userString;
        return AuthResult::fail(Protocol::ServerGreetingCode::LoginFailed);
    }

    userString[usernameLen] = 0;
    std::string username(userString);
    delete[] userString;

    UserDB::User userInfo;
    if (!userDB.getUserInfo(username, userInfo))
    {
        return AuthResult::fail(Protocol::ServerGreetingCode::SessionExpired);
    }

    if (userInfo.state != UserDB::UserState::KnownSessionID)
    {
        return AuthResult::fail(Protocol::ServerGreetingCode::SessionExpired);
    }

    uint64_t now = (uint64_t) time(NULL);
    if (userInfo.expiryTime < now)
    {
        return AuthResult::fail(Protocol::ServerGreetingCode::SessionExpired);
    }

    if (userInfo.sid != sid)
    {
        return AuthResult::fail(Protocol::ServerGreetingCode::SessionExpired);
    }

    return AuthResult::success(username, sid, userInfo.perms);
}

static AuthResult handlePasswordAuth(SOCKET clientSocket, Protocol::LoginHeader &loginHeader)
{
    if (loginHeader.payloadSize < 1)
    {
        return AuthResult::fail(Protocol::ServerGreetingCode::LoginFailed);
    }

    uint8_t usernameLen;
    if (!readSocket(clientSocket, (char*) &usernameLen, 1))
    {
        return AuthResult::fail(Protocol::ServerGreetingCode::LoginFailed);
    }

    size_t passwordLen = loginHeader.payloadSize-1-usernameLen;

    char *userString = new char[usernameLen+1];
    if (!readSocket(clientSocket, userString, usernameLen))
    {
        delete[] userString;
        return AuthResult::fail(Protocol::ServerGreetingCode::LoginFailed);
    }

    userString[usernameLen] = 0;
    std::string username(userString);
    delete[] userString;

    char *passwordString = new char[passwordLen+1];
    if (!readSocket(clientSocket, passwordString, passwordLen))
    {
        delete[] passwordString;
        return AuthResult::fail(Protocol::ServerGreetingCode::LoginFailed);
    }

    passwordString[passwordLen] = 0;
    std::string password(passwordString);
    delete[] passwordString;

    UserDB::User userInfo;
    if (!userDB.startSession(username, password, userInfo))
    {
        return AuthResult::fail(Protocol::ServerGreetingCode::WrongUsernameOrPassword);
    }

    if (userInfo.state == UserDB::UserState::Banned)
    {
        return AuthResult::fail(Protocol::ServerGreetingCode::UserBanned);
    }

    if (userInfo.state == UserDB::UserState::Pending)
    {
        return AuthResult::fail(Protocol::ServerGreetingCode::UserPending);
    }

    return AuthResult::success(username, userInfo.sid, userInfo.perms);
}

static AuthResult handleAuth(SOCKET clientSocket, Protocol::LoginHeader &loginHeader)
{
    switch (loginHeader.authType)
    {
    case Protocol::AuthenticationType::SessionID:   return handleSessionAuth(clientSocket, loginHeader);
    case Protocol::AuthenticationType::Password:    return handlePasswordAuth(clientSocket, loginHeader);
    default:                                        return AuthResult::fail(Protocol::ServerGreetingCode::LoginFailed);
    }
}

static void respondToLogin(SOCKET clientSocket, Version clientVersion)
{
    if (clientVersion != Version::ofThisBuild)
    {
        Protocol::ServerGreetingHeader response(Protocol::ServerGreetingCode::IncompatibleVersion);
        writeSocket(clientSocket, (char*) &response, sizeof(response));
        closesocket(clientSocket);
        return;
    }

    Protocol::LoginHeader loginHeader;
    if (!readSocket(clientSocket, (char*) &loginHeader, sizeof(loginHeader)))
    {
        closesocket(clientSocket);
        return;
    }

    AuthResult authResult = handleAuth(clientSocket, loginHeader);
    if (!authResult.isSuccess)
    {
        Protocol::ServerGreetingHeader response(authResult.respcode);
        writeSocket(clientSocket, (char*) &response, sizeof(response));
        closesocket(clientSocket);
        return;
    }

    serverCommandQueue.add([clientSocket, authResult](ServerWorld &world)
    {
        std::shared_ptr<Player> player = world.addPlayer(authResult.username, authResult.perms);
        if (player == nullptr)
        {
            Protocol::ServerGreetingHeader response(Protocol::ServerGreetingCode::AlreadyLoggedIn);
            writeSocket(clientSocket, (char*) &response, sizeof(response));
            closesocket(clientSocket);

            Log::error(formatString(
                "User '$+' attempted duplicate login from $+!",
                authResult.username,
                getSocketRemoteAddr(clientSocket)
            ));
            return;
        }
    
        Protocol::ServerGreetingHeader response(Protocol::ServerGreetingCode::LoginOK);
        writeSocket(clientSocket, (char*) &response, sizeof(response));

        Log::info(formatString(
            "User '$+' logged in from $+.",
            authResult.username,
            getSocketRemoteAddr(clientSocket)
        ));

        // Send the session ID.
        writeSocket(clientSocket, (const char*) &authResult.sid, sizeof(authResult.sid));

        // Send the weather seed.
        GeneratorNode weatherNode = world.weather.genNode;
        writeSocket(clientSocket, (const char*) &weatherNode, sizeof(weatherNode));

        // Send the world time for synchronization of phenomena.
        uint64_t worldTime = world.getWorldTime();
        writeSocket(clientSocket, (const char*) &worldTime, 8);

        // Send player data.
        std::vector<uint8_t> data;
        player->serializeState(data);
        uint32_t sizeSpec = data.size();
        writeSocket(clientSocket, (const char*) &player->id, 8);
        writeSocket(clientSocket, (char*) &sizeSpec, 4);
        writeSocket(clientSocket, (char*) data.data(), data.size());

        // Send the block table.
        std::vector<std::string> &blockTable = world.getBlockTable();
        std::vector<std::string>::iterator it;
        for (it=blockTable.begin(); it!=blockTable.end(); ++it)
        {
            uint8_t sizeByte = it->size();
            writeSocket(clientSocket, (char*) &sizeByte, 1);
            writeSocket(clientSocket, it->c_str(), it->size());
        }

        writeSocket(clientSocket, "", 1);

        // Send the time of day.
        uint32_t timeOfDay = (uint32_t) world.getTimeOfDay();
        writeSocket(clientSocket, (const char*) &timeOfDay, 4);

        // Send the belts.
        std::vector<uint8_t> beltData;
        world.serializeBeltViews(beltData);
        uint32_t beltDataSize = beltData.size();

        writeSocket(clientSocket, (const char*) &beltDataSize, 4);
        writeSocket(clientSocket, (const char*) beltData.data(), beltData.size());

        // Inform other clients of this player.
        world.sendCreateEntityMessageToClients(player);

        // Create the player messenger.
        std::shared_ptr<Messenger> messenger = std::make_shared<Messenger>(clientSocket);
        player->client = messenger;
        
        // Inform this client of other entities.
        world.forEachEntity([messenger, player](std::shared_ptr<Entity> entity)
        {
            if (entity != player)
            {
                messenger->sendCreateEntityMessage(entity);
            }
        });

        // Attach the gadget controllers.
        for (size_t i = 0; i < Player::numEquipmentSlots; i++)
        {
            player->reattachGadget(i);
        }

        std::thread thread(receivingThread, authResult.username, player);
        thread.detach();
    });
}

static void respondToRegister(SOCKET clientSocket, Version clientVersion)
{
    if (clientVersion != Version::ofThisBuild)
    {
        Protocol::ServerGreetingHeader response(Protocol::ServerGreetingCode::IncompatibleVersion);
        writeSocket(clientSocket, (char*) &response, sizeof(response));
        closesocket(clientSocket);
        return;
    }

    Protocol::RegisterHeader registerHeader;
    if (!readSocket(clientSocket, (char*) &registerHeader, sizeof(registerHeader)))
    {
        closesocket(clientSocket);
        return;
    }

    char *userString = new char[registerHeader.userNameSize+1];
    if (!readSocket(clientSocket, userString, registerHeader.userNameSize))
    {
        delete[] userString;
        closesocket(clientSocket);
        return;
    }

    userString[registerHeader.userNameSize] = 0;
    std::string username(userString);
    delete[] userString;

    char *passwordString = new char[registerHeader.passwordSize+1];
    if (!readSocket(clientSocket, passwordString, registerHeader.passwordSize))
    {
        delete[] passwordString;
        closesocket(clientSocket);
        return;
    }

    passwordString[registerHeader.passwordSize] = 0;
    std::string password(passwordString);
    delete[] passwordString;

    std::string remoteAddr = getSocketRemoteAddr(clientSocket);

    // Try to create the account.
    UserDB::CreateUserResult result = userDB.createUser(username, password);

    if (result.created)
    {
        Log::info(formatString("Created account for '$+' from $+: $+", username, remoteAddr, result.message));
    }
    else
    {
        Log::error(formatString("Refused account for '$+' from $+: $+", username, remoteAddr, result.message));
    }

    Protocol::ServerGreetingHeader response(
        result.created
            ? Protocol::ServerGreetingCode::AccountCreated
            : Protocol::ServerGreetingCode::AccountNotCreated
    );

    writeSocket(clientSocket, (char*) &response, sizeof(response));
    
    uint8_t messageSize = result.message.size();
    writeSocket(clientSocket, (char*) &messageSize, 1);
    writeSocket(clientSocket, result.message.c_str(), result.message.size());

    closesocket(clientSocket);
}

static void respondToUnknownGreeting(SOCKET clientSocket, Version clientVersion)
{
    // If we receive an unknown greeting from a different client version, we send back an IncompatibleVersion
    // response, because it could be a greeting that's not supported by our version. However, if the versions
    // match, it is a protocol error to send an invalid greeting, so in this case we treat the client as broken
    // and simply close the connection.
    if (clientVersion != Version::ofThisBuild)
    {
        Protocol::ServerGreetingHeader response(Protocol::ServerGreetingCode::IncompatibleVersion);
        writeSocket(clientSocket, (char*) &response, sizeof(response));
        return;
    }
}

static void clientThread(SOCKET clientSocket)
{
    Protocol::ClientGreetingHeader greetingHeader;
    if (!readSocket(clientSocket, (char*) &greetingHeader, sizeof(greetingHeader)))
    {
        closesocket(clientSocket);
        return;
    }

    if (!greetingHeader.isValid())
    {
        closesocket(clientSocket);
        return;
    }

    switch (greetingHeader.code)
    {
    case Protocol::ClientGreetingCode::Ping:
        respondToPing(clientSocket);
        closesocket(clientSocket);
        break;
    case Protocol::ClientGreetingCode::Login:
        respondToLogin(clientSocket, greetingHeader.version);
        break;
    case Protocol::ClientGreetingCode::Register:
        respondToRegister(clientSocket, greetingHeader.version);
        break;
    default:
        respondToUnknownGreeting(clientSocket, greetingHeader.version);
        closesocket(clientSocket);
        break;
    }
}

int startServer()
{
    if (!SDL_Init(0))
    {
        Log::error("Failed to init SDL.");
        return 1;
    }

    Log::info("Starting Voxelverse server...");
    if (!std::filesystem::exists("server/world.voxelverse") || !std::filesystem::exists("server/users.db"))
    {
        if (isOutputtingToConsole())
        {
            Log::warn("The server is not currently initialized, starting the initializer tool.");
            serverInit();
        }
        else
        {
            crash("The server is not initialized; run under a console to initialize.");
        }
    }

    userDB.open();

    SOCKET serverSocket = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
    if (serverSocket == INVALID_SOCKET)
    {
        Log::error("Failed to create TCP socket");
        return 1;
    }

    int v6only = 0;
    setsockopt(serverSocket, IPPROTO_IPV6, IPV6_V6ONLY, (char*) &v6only, sizeof(v6only));

    int nodelay = 1;
    setsockopt(serverSocket, IPPROTO_TCP, TCP_NODELAY, (char*) &nodelay, sizeof(nodelay));

    int reuseaddr = 1;
    setsockopt(serverSocket, SOL_SOCKET, SO_REUSEADDR, (char*) &reuseaddr, sizeof(reuseaddr));

    if (!setSocketBlocking(serverSocket, false))
    {
        closesocket(serverSocket);
        Log::error("Failed to make server socket nonblocking");
        return 1;
    }

    struct sockaddr_in6 addr;
    memset(&addr, 0, sizeof(struct sockaddr_in6));
    addr.sin6_family = AF_INET6;
    addr.sin6_port = htons(settings.serverPort);

    if (bind(serverSocket, (struct sockaddr*) &addr, sizeof(struct sockaddr_in6)) != 0)
    {
        closesocket(serverSocket);
        Log::error(formatString("Failed to bind to server port $+", settings.serverPort));
        return 1;
    }

    if (listen(serverSocket, 5) != 0)
    {
        closesocket(serverSocket);
        Log::error("Failed to initiate listening on the server socket");
        return 1;
    }

    Log::info(formatString("Successfully opened server socket on port $+", settings.serverPort));

    signal(SIGINT, onSigInt);

    auto scheduleTask = [](ServerWorld::TaskCallback callback)
    {
        serverCommandQueue.add([callback](ServerWorld&)
        {
            callback();
        });
    };

    ServerWorld world("server/world.voxelverse", scheduleTask);

    Log::info("World initialization complete.");

    Uint32 nextUpdateTicks = 0;
    Uint32 lastUpdateTicks = 0;
    std::string fatalError;

    while (running)
    {
        fatalError = world.getFatalError();
        if (fatalError.size() != 0)
        {
            break;
        }

        SOCKET clientSocket = accept(serverSocket, NULL, NULL);
        if (clientSocket != (SOCKET)SOCKET_ERROR)
        {
            std::thread thread(clientThread, clientSocket);
            thread.detach();
        }

        serverCommandQueue.processAllCommands(world);

        world.fastUpdate(nullptr);

        Uint32 currentUpdateTicks = SDL_GetTicks();
        if (currentUpdateTicks >= nextUpdateTicks)
        {
            int numUpdatesToPerform = (currentUpdateTicks - lastUpdateTicks) / updateDuration;
            if (numUpdatesToPerform > 16)
            {
                Log::warn(formatString("Skipped $+ updates! Only 16 will be performed!", numUpdatesToPerform));
                numUpdatesToPerform = 16;
            }

            for (int i=0; i<numUpdatesToPerform; i++)
            {
                world.update();
            }

            nextUpdateTicks = ((currentUpdateTicks / updateDuration) + 1) * updateDuration;
            lastUpdateTicks = currentUpdateTicks;
        }
    }

    Log::info("Shutting down server...");
    world.close();

    while (world.getPurgeQueueSize() != 0 || !world.isShutdownReady())
    {
        world.housekeep();
        serverCommandQueue.processAllCommands(world);
    }

    closesocket(serverSocket);

    bool tasksFinished = false;
    std::condition_variable cond;
    std::mutex mutex;

    afterAsyncBarrier(AsyncBarrier::CurrentlyDispatched, [&tasksFinished, &cond, &mutex]()
    {
        std::unique_lock<std::mutex> lock(mutex);

        tasksFinished = true;
        lock.unlock();
        cond.notify_one();
    });

    std::unique_lock<std::mutex> lock(mutex);
    while (!tasksFinished)
    {
        cond.wait(lock);
    }

    lock.unlock();
    world.doShutdownSanityChecks();
    Log::info("Shutdown complete.");

    if (fatalError.size() != 0)
    {
        crash(formatString("Server stopped due to fatal error in the world: $+", fatalError));
    }

    return 0;
}