#include "coduo_engine_structs.h"
#include "../abi/vm_private.h"
#include "../core_command/cmd_private.h"
#include "../core_cvar/cvar_private.h"
#include "../core_info/info_private.h"
#include "../core_runtime/core_runtime_private.h"
#include "../filesystem/fs_private.h"
#include "../networking/netchan_private.h"
#include "../punkbuster/pb_private.h"
#include "server_private.h"
#include "sv_globals_private.h"

#include <stdlib.h>
#include <stdio.h>
#include <strings.h>
#include <string.h>

enum {
    SV_AUTHORIZE_GUID_RECENT_TIMEOUT_MS = 1000,
    SV_CONNECT_PROTOCOL = 22,
    SV_CONNECT_MAX_INFO_STRING = 0x400
};

qboolean SV_AuthorizeGuidRecentlySeen(int32_t guid)
{
    if (guid == 0) {
        return qfalse;
    }

    for (int32_t slot = 0;
         slot < CODUO_AUTHORIZE_GUID_CACHE_ENTRY_COUNT; ++slot) {
        const coduo_authorize_guid_cache_entry_t *entry =
            &host_authorizeGuidCache[slot];

        /* 0x8089dc0: the time delta enters the compare as a bare fild (kept
         * exact in 80-bit).  The timeout multiply also remains live, and
         * 0x8089dde accepts equality and unordered results. */
        if (entry->guid == guid) {
            long double age =
                (long double)(svs.time - entry->time);
            long double timeout =
                (long double)host_cvar_sv_kickBanTime_pointer_slot->value *
                (long double)(float)SV_AUTHORIZE_GUID_RECENT_TIMEOUT_MS;

            if (!(age > timeout)) {
                return qtrue;
            }
        }
    }

    return qfalse;
}

qboolean SV_AuthorizeGuidOnBanList(int32_t guid)
{
    void *banFile;
    char *scan;
    qboolean found;

    if (guid == 0) {
        return qfalse;
    }

    if (FS_ReadFile("ban.txt", &banFile) < 0) {
        return qfalse;
    }

    scan = (char *)banFile;
    found = qfalse;
    while (1) {
        char *token = Com_Parse(&scan);
        if (token[0] == '\0') {
            break;
        }
        if (atoi(token) == guid) {
            found = qtrue;
            break;
        }
        Com_SkipRestOfLine(&scan);
    }

    FS_FreeFile(banFile);
    return found;
}

int32_t SV_AuthorizeGuidCacheSelectSlot(void)
{
    int32_t oldestSlot = 0;

    for (int32_t slot = 0;
         slot < CODUO_AUTHORIZE_GUID_CACHE_ENTRY_COUNT; ++slot) {
        if (host_authorizeGuidCache[slot].guid == 0) {
            return slot;
        }
        if (host_authorizeGuidCache[slot].time <
            host_authorizeGuidCache[oldestSlot].time) {
            oldestSlot = slot;
        }
    }

    return oldestSlot;
}

void SV_AuthorizeGuidCacheStore(int32_t guid)
{
    int32_t slot =
        SV_AuthorizeGuidCacheSelectSlot();

    host_authorizeGuidCache[slot].guid = guid;
    host_authorizeGuidCache[slot].time = svs.time;
}

qboolean SV_AuthorizeIsCrackedMode(void)
{
    return host_cvar_sv_cracked_pointer_slot->integer == 1;
}

static void SV_AuthorizeClearChallenge(challenge_t *challenge)
{
    /* NOT_FROM_ORIGINAL_SOURCE: local wrapper for repeated stock challenge-slot clears. */
    memset(challenge, 0, sizeof(*challenge));
}

static int32_t SV_ConnectClientNum(client_t *client)
{
    /* NOT_FROM_ORIGINAL_SOURCE: local typed expression of stock client stride math. */
    return (int32_t)(client - svs.clients);
}

static qboolean SV_ConnectMatchesClient(netadr_t from,
                                        const client_t *client,
                                        int qport)
{
    /* NOT_FROM_ORIGINAL_SOURCE: local wrapper for the repeated reconnect address test. */
    return NET_CompareBaseAdr(from, client->netchan.remoteAddress) &&
           (client->netchan.qport == qport ||
            from.port == client->netchan.remoteAddress.port);
}

void SV_AuthorizeIpPacket(netadr_t from)
{
    int32_t challengeNumber;
    challenge_t *challenge;
    int challengeIndex;
    const char *response;
    const char *reason;
    char error[1024];

    if (!NET_CompareAdr(from, host_authorizeServerAddress)) {
        Com_Printf("SV_AuthorizeIpPacket: not from authorize server\n");
        return;
    }

    challengeNumber = atoi(Cmd_Argv(1));
    for (challengeIndex = 0; challengeIndex < CODUO_HOST_CHALLENGE_COUNT;
         ++challengeIndex) {
        if (host_svs_challenges[challengeIndex].challenge == challengeNumber) {
            break;
        }
    }

    if (challengeIndex == CODUO_HOST_CHALLENGE_COUNT) {
        Com_Printf("SV_AuthorizeIpPacket: challenge not found\n");
        return;
    }

    challenge = &host_svs_challenges[challengeIndex];
    challenge->pingTime = svs.timeSecondary;
    response = Cmd_Argv(2);
    reason = Cmd_Argv(3);
    Q_strncpyz(challenge->authGuid, Cmd_Argv(5),
                 sizeof(challenge->authGuid));

    if (Q_stricmp(response, "demo") == 0) {
        if (Cvar_VariableValue("fs_restrict") != 0.0f) {
            NET_OutOfBandPrint(NS_SERVER, challenge->adr,
                               "challengeResponse %i", challenge->challenge);
        } else {
            NET_OutOfBandPrint(NS_SERVER, challenge->adr,
                               "error\nEXE_ERR_NOT_A_DEMO_SERVER");
            SV_AuthorizeClearChallenge(challenge);
        }
        return;
    }

    if (Q_stricmp(response, "accept") == 0) {
        challenge->guid = atoi(Cmd_Argv(4));
        if (SV_AuthorizeGuidOnBanList(challenge->guid)) {
            Com_Printf("rejected connection from permanently banned GUID %i\n",
                       challenge->guid);
            NET_OutOfBandPrint(NS_SERVER, challenge->adr,
                               "error\n\x15"
                               "You are permanently banned from this server");
            SV_AuthorizeClearChallenge(challenge);
            return;
        }
        if (SV_AuthorizeGuidRecentlySeen(challenge->guid)) {
            Com_Printf("rejected connection from temporarily banned GUID %i\n",
                       challenge->guid);
            NET_OutOfBandPrint(NS_SERVER, challenge->adr,
                               "error\n\x15"
                               "You are temporarily banned from this server");
            SV_AuthorizeClearChallenge(challenge);
            return;
        }
        if (!challenge->connected) {
            if (host_cvar_sv_onlyVisibleClients_pointer_slot->integer == 0) {
                NET_OutOfBandPrint(NS_SERVER, challenge->adr,
                                   "challengeResponse %i",
                                   challenge->challenge);
            } else {
                NET_OutOfBandPrint(NS_SERVER, challenge->adr,
                                   "challengeResponse %i %i",
                                   challenge->challenge,
                                   host_cvar_sv_onlyVisibleClients_pointer_slot
                                       ->integer);
            }
        }
        return;
    }

    if (Q_stricmp(response, "deny") == 0) {
        if (reason == NULL || reason[0] == '\0') {
            NET_OutOfBandPrint(NS_SERVER, challenge->adr,
                               "error\nEXE_ERR_CDKEY_IN_USE");
        } else if (Q_stricmp(reason, "CLIENT_UNKNOWN_TO_AUTH") == 0 ||
                   Q_stricmp(reason, "BAD_CDKEY") == 0) {
            NET_OutOfBandPrint(NS_SERVER, challenge->adr,
                               "needcdkey");
        } else if (Q_stricmp(reason, "INVALID_CDKEY") == 0) {
            NET_OutOfBandPrint(NS_SERVER, challenge->adr,
                               "error\nEXE_ERR_CDKEY_IN_USE");
        } else {
            NET_OutOfBandPrint(NS_SERVER, challenge->adr,
                               "error\nEXE_ERR_BAD_CDKEY");
        }
        SV_AuthorizeClearChallenge(challenge);
        return;
    }

    if (reason == NULL || reason[0] == '\0') {
        NET_OutOfBandPrint(NS_SERVER, challenge->adr,
                           "error\nEXE_ERR_BAD_CDKEY");
    } else {
        /* ORIGINAL_BINARY_BUG: 0x0808ab00..0x0808ab18 prefixes an
         * unrestricted authorization reason into this 1,024-byte array with
         * sprintf; see
         * original-bugs/coduomp-out-of-band-formatting-overflow.md. */
        sprintf(error, "error\n%s", reason);
        NET_OutOfBandPrint(NS_SERVER, challenge->adr, "%s", error);
    }
    SV_AuthorizeClearChallenge(challenge);
}

void SVC_DirectConnect(netadr_t from)
{
    char userinfo[SV_CONNECT_MAX_INFO_STRING];
    int32_t challengeNumber;
    int qport;
    int32_t guid;
    int challengeIndex;
    int ping;
    client_t *client;
    client_t emptyClient;
    int32_t clientNum;
    int clPunkbuster;
    const char *clGuid;
    const char *pbReject;
    const char *gameReject;
    const char *password;
    int firstAvailableClient;
    int connectedCount;

    Com_DPrintf("SVC_DirectConnect ()\n");
    Q_strncpyz(userinfo, Cmd_Argv(1), SV_CONNECT_MAX_INFO_STRING);

    if (atoi(Info_ValueForKey(userinfo, "protocol")) != SV_CONNECT_PROTOCOL) {
        int protocol = atoi(Info_ValueForKey(userinfo, "protocol"));

        NET_OutOfBandPrint(NS_SERVER, from, "error\n%s",
                           "EXE_SERVER_IS_DIFFERENT_VER");
        Com_DPrintf("    rejected connect from protocol version %i (should be %i)\n",
                    protocol, SV_CONNECT_PROTOCOL);
        return;
    }

    challengeNumber = atoi(Info_ValueForKey(userinfo, "challenge"));
    qport = atoi(Info_ValueForKey(userinfo, "qport"));

    client = svs.clients;
    for (clientNum = 0; clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum, ++client) {
        if (SV_ConnectMatchesClient(from, client, qport)) {
            if (svs.timeSecondary - client->lastConnectTime <
                host_cvar_sv_reconnectlimit_pointer_slot->integer *
                    SV_AUTHORIZE_GUID_RECENT_TIMEOUT_MS) {
                Com_DPrintf("%s:reconnect rejected : too soon\n",
                            NET_AdrToString(from));
                return;
            }
            break;
        }
    }

    guid = 0;
    challengeIndex = 0;
    if (!NET_IsLocalAddress(from)) {
        for (challengeIndex = 0; challengeIndex < CODUO_HOST_CHALLENGE_COUNT;
             ++challengeIndex) {
            challenge_t *challenge = &host_svs_challenges[challengeIndex];

            if (NET_CompareBaseAdr(from, challenge->adr) &&
                challengeNumber == challenge->challenge) {
                guid = challenge->guid;
                break;
            }
        }

        if (challengeIndex == CODUO_HOST_CHALLENGE_COUNT) {
            NET_OutOfBandPrint(NS_SERVER, from,
                               "error\nEXE_BAD_CHALLENGE");
            return;
        }

        if (host_svs_challenges[challengeIndex].firstPing == 0) {
            ping = svs.timeSecondary -
                   host_svs_challenges[challengeIndex].pingTime;
            host_svs_challenges[challengeIndex].firstPing = ping;
        } else {
            ping = host_svs_challenges[challengeIndex].firstPing;
        }

        Com_Printf("Client %i connecting with %i challenge ping from %s\n",
                   challengeIndex, ping, NET_AdrToString(from));
        host_svs_challenges[challengeIndex].connected = qtrue;

        if (!Sys_IsLANAddress(from)) {
            /* 0x808b179 / 0x808b1f9: ping enters both compares as a bare
             * fild (exact in 80-bit); a (float) cast would round it. */
            if (host_cvar_sv_minPing_pointer_slot->value != 0.0f &&
                ping < host_cvar_sv_minPing_pointer_slot->value) {
                NET_OutOfBandPrint(NS_SERVER, from,
                                   "error\nEXE_ERR_HIGH_PING_ONLY");
                Com_DPrintf("Client %i rejected on a too low ping\n",
                            challengeIndex);
                return;
            }
            if (host_cvar_sv_maxPing_pointer_slot->value != 0.0f &&
                host_cvar_sv_maxPing_pointer_slot->value < ping) {
                NET_OutOfBandPrint(NS_SERVER, from,
                                   "error\nEXE_ERR_LOW_PING_ONLY");
                Com_DPrintf("Client %i rejected on a too high ping: %i\n",
                            challengeIndex, ping);
                return;
            }
        }
    }

    clPunkbuster = atoi(Info_ValueForKey(userinfo, "cl_punkbuster"));
    clGuid = Info_ValueForKey(userinfo, "cl_guid");
    pbReject = PB_InvokeStringQueryCallback(
        NET_IsLocalAddress(from) ? "localhost" : NET_AdrToString(from),
        clPunkbuster, clGuid);
    if (pbReject != NULL) {
        if (strncasecmp(pbReject, "error\n", 6) == 0) {
            NET_OutOfBandPrint(NS_SERVER, from, "%s", pbReject);
        }
        return;
    }

    memset(&emptyClient, 0, sizeof(emptyClient));

    client = svs.clients;
    for (clientNum = 0; clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum, ++client) {
        if (client->state != CODUO_CS_FREE &&
            SV_ConnectMatchesClient(from, client, qport)) {
            Com_Printf("%s:reconnect\n", NET_AdrToString(from));
            if (client->state > CODUO_CS_ZOMBIE) {
                PB_DropClient(client);
            }
            break;
        }
    }

    if (clientNum == host_cvar_sv_maxclients_pointer_slot->integer) {
        password = Info_ValueForKey(userinfo, "password");
        firstAvailableClient =
            strcmp(password, host_cvar_sv_privatePassword_pointer_slot->string) == 0
                ? 0
                : host_cvar_sv_privateClients_pointer_slot->integer;

        client = NULL;
        for (clientNum = firstAvailableClient;
             clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
             ++clientNum) {
            client_t *candidate = &svs.clients[clientNum];

            if (candidate->state == CODUO_CS_FREE) {
                client = candidate;
                break;
            }
        }

        if (client == NULL) {
            NET_OutOfBandPrint(NS_SERVER, from,
                               "error\nEXE_SERVERISFULL");
            Com_DPrintf("Rejected a connection.\n");
            return;
        }

        client->reliableAcknowledge = 0;
        client->reliableSequence = 0;
    }

    *client = emptyClient;
    clientNum = SV_ConnectClientNum(client);
    client->gentity = SV_GentityNum(clientNum);
    client->scriptId = coduomp_sv_alloc_client_script_pers();
    client->challenge = challengeNumber;
    client->guid = guid;
    Netchan_Setup(NS_SERVER, &client->netchan, from, qport);
    Q_strncpyz(client->userinfo, userinfo, SV_CONNECT_MAX_INFO_STRING);

    gameReject =
        (const char *)VM_Call(sv_gameVM, CODUO_GAME_CLIENT_CONNECT, clientNum,
                              client->scriptId);
    if (gameReject != NULL) {
        NET_OutOfBandPrint(NS_SERVER, from, "error\n%s",
                           gameReject);
        Com_DPrintf("Game rejected a connection: %s.\n", gameReject);
        SV_FreeClientScriptId(client);
        return;
    }

    SV_UserinfoChanged(client);
    /* ORIGINAL_BINARY_BUG: 0x0808b846..0x0808b862 indexes the challenge
     * table with the selected client slot, not the matched challenge slot;
     * see original-bugs/coduomp-direct-connect-challenge-index.md. */
    host_svs_challenges[clientNum].firstPing = 0;
    NET_OutOfBandPrint(NS_SERVER, from, "connectResponse");
    Com_Printf("Going from CS_FREE to CS_CONNECTED for %s (num %i guid %i)\n",
               client->name, clientNum, client->guid);
    client->state = CODUO_CS_CONNECTED;
    client->lastPacketTime = svs.timeSecondary;
    client->lastConnectTime = svs.timeSecondary;
    client->nextSnapshotTime = svs.timeSecondary;
    client->gamestateMessageNum = -1;

    connectedCount = 0;
    for (clientNum = 0; clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum) {
        if (svs.clients[clientNum].state > CODUO_CS_ZOMBIE) {
            ++connectedCount;
        }
    }

    if (connectedCount == 1 ||
        connectedCount == host_cvar_sv_maxclients_pointer_slot->integer) {
        SV_Heartbeat_f();
    }
}
