/* bzflag
 * Copyright (c) 1993-2025 Tim Riker
 *
 * This package is free software;  you can redistribute it and/or
 * modify it under the terms of the license found in the file
 * named COPYING that should have accompanied this file.
 *
 * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

/* interface header */
#include "BZAdminClient.h"

/* system implementation headers */
#ifdef HAVE_CMATH
#  include <cmath>
#else
#  include <math.h>
#endif
#include <stdio.h>
#include <string.h>
#include <iostream>
#include <sstream>

/* common implementation headers */
#include "BZAdminUI.h"
#include "StateDatabase.h"
#include "TextUtils.h"
#include "version.h"
#include "Team.h"
#include "ServerAuth.h"
#include "ServerList.h"
#include "ErrorHandler.h"
#include "cURLManager.h"
#include "TimeKeeper.h"

StartupInfo startupInfo;

BZAdminClient::BZAdminClient(BZAdminUI* bzInterface)
    : myTeam(ObserverTeam), sLink(Address(startupInfo.serverName), startupInfo.serverPort), valid(false), ui(bzInterface)
{

    if (sLink.getState() != ServerLink::Okay)
    {
        switch (sLink.getState())
        {
        case ServerLink::BadVersion:
        {
            std::cout << "Incompatible server version " << sLink.getVersion();
            break;
        }
        case ServerLink::Refused:
        {
            std::string banMessage = "Server Refused connection due to ban: ";
            banMessage += sLink.getRejectionMessage();
            std::cout << banMessage;
            break;
        }
        case ServerLink::Rejected:
            std::cout << "Game is full or over.  Try again later.";
            break;
        case ServerLink::SocketError:
            std::cout << "Error connecting to server.";
            break;
        case ServerLink::CrippledVersion:
            std::cout << "Cannot connect to full version server.";
            break;
        default:
            std::cout << "Internal error connecting to server.";
            break;
        }
        std::cout << std::endl;
        return;
    }

    // If a password was provided, get a token
    if (startupInfo.password[0] != '\0')
    {
        ServerAuth* serverAuth = new ServerAuth;
        serverAuth->requestToken(&startupInfo);
        // wait no more than 10 seconds for a token
        for (int j = 0; j < 40; j++)
        {
            cURLManager::perform();
            if (startupInfo.token[0] != '\0') break;
            TimeKeeper::sleep(0.25f);
        }
        delete serverAuth;

        // don't let the bad token specifier slip through to the server,
        // just erase it
        if (strcmp(startupInfo.token, "badtoken") == 0)
            startupInfo.token[0] = '\0';
    }
    sLink.sendEnter(TankPlayer, myTeam, startupInfo.callsign, "bzadmin", startupInfo.token);
    if (sLink.getState() != ServerLink::Okay)
    {
        std::cerr << "Rejected." << std::endl;
        return;
    }

    std::string reason;
    uint16_t code, rejcode;
    if (sLink.readEnter (reason, code, rejcode))
        valid = true;
    else
        std::cerr << reason << std::endl;


    // tell BZDB to shut up, we can't have debug data printed to stdout
    BZDB.setDebug(false);

    // set a default message mask
    showMessageType(MsgAddPlayer);
    showMessageType(MsgAdminInfo);
    showMessageType(MsgKilled);
    showMessageType(MsgMessage);
    showMessageType(MsgNewRabbit);
    showMessageType(MsgPause);
    showMessageType(MsgRemovePlayer);
    showMessageType(MsgSuperKill);

    // initialise the colormap
    colorMap[NoTeam] = Yellow;
    colorMap[RogueTeam] = Yellow;
    colorMap[RedTeam] = Red;
    colorMap[GreenTeam] = Green;
    colorMap[BlueTeam] = Blue;
    colorMap[PurpleTeam] = Purple;
    colorMap[ObserverTeam] = Cyan;

    // initialise the msg type map
    // FIXME MsgPlayerInfo
    msgTypeMap["bzdb"] = MsgSetVar;
    msgTypeMap["chat"] = MsgMessage;
    msgTypeMap["admin"] = MsgAdminInfo;
    msgTypeMap["join"] = MsgAddPlayer;
    msgTypeMap["kill"] = MsgKilled;
    msgTypeMap["leave"] = MsgRemovePlayer;
    msgTypeMap["pause"] = MsgPause;
    msgTypeMap["ping"] = MsgLagPing;
    msgTypeMap["rabbit"] = MsgNewRabbit;
    msgTypeMap["score"] = MsgScore;
    msgTypeMap["spawn"] = MsgAlive;
    msgTypeMap["time"] = MsgTimeUpdate;
    msgTypeMap["over"] = MsgScoreOver;
}


PlayerId BZAdminClient::getMyId()
{
    return sLink.getId();
}


BZAdminClient::ServerCode BZAdminClient::checkMessage()
{
    uint16_t code, len;
    char inbuf[MaxPacketLen];
    PlayerIdMap::iterator iter;

    // read until we have a package, or until we have waited 100 ms
    if (sLink.read(code, len, inbuf, 100) == 1)
    {
        lastMessage.first = "";
        lastMessage.second = Default;
        const void* vbuf = inbuf;
        PlayerId p;
        PlayerIdMap::const_iterator it;
        std::string victimName, killerName;
        Address a;

        switch (code)
        {

        case MsgNewRabbit:
            if (messageMask[MsgNewRabbit])
            {
                vbuf = nboUnpackUByte(vbuf, p);
                if (p != NoPlayer)
                    lastMessage.first = std::string("*** '") + players[p].name +
                                        "' is now the rabbit.";
            }
            break;

        case MsgPause:
            if (messageMask[MsgPause])
            {
                uint8_t paused;
                vbuf = nboUnpackUByte(vbuf, p);
                vbuf = nboUnpackUByte(vbuf, paused);
                lastMessage.first = std::string("*** '") + players[p].name + "': " +
                                    (paused ? "paused" : "resumed") + ".";
            }
            break;

        case MsgAlive:
            if (messageMask[MsgAlive])
            {
                vbuf = nboUnpackUByte(vbuf, p);
                lastMessage.first = std::string("*** '") + players[p].name +
                                    "' has respawned.";
            }
            break;

        case MsgLagPing:
            if (messageMask[MsgLagPing])
                lastMessage.first = "*** Received lag ping from server.";
            break;

        case MsgSetVar:
            // code stolen from playing.cxx
            uint16_t numVars;
            uint8_t nameLen, valueLen;

            // Flawfinder: ignore
            char name[MaxPacketLen];
            // Flawfinder: ignore
            char value[MaxPacketLen];
            int i;

            vbuf = nboUnpackUShort(vbuf, numVars);
            for (i = 0; i < numVars; i++)
            {
                vbuf = nboUnpackUByte(vbuf, nameLen);
                vbuf = nboUnpackString(vbuf, name, nameLen);
                name[nameLen] = '\0';

                vbuf = nboUnpackUByte(vbuf, valueLen);
                vbuf = nboUnpackString(vbuf, value, valueLen);
                value[valueLen] = '\0';

                BZDB.set(name, value);
                BZDB.setPersistent(name, false);
                BZDB.setPermission(name, StateDatabase::Locked);
            }
            if (messageMask[MsgSetVar])
            {
                lastMessage.first = std::string("*** Received BZDB update, ") +
                                    TextUtils::format("%d", numVars) + " variable" +
                                    (numVars == 1 ? "" : "s") + " updated.";
            }
            break;

        case MsgAddPlayer:
            uint16_t team, type, wins, losses, tks;
            // Flawfinder: ignore
            char callsign[CallSignLen];
            // Flawfinder: ignore
            char motto[MottoLen];
            vbuf = nboUnpackUByte(vbuf, p);
            vbuf = nboUnpackUShort(vbuf, type);
            vbuf = nboUnpackUShort(vbuf, team);
            vbuf = nboUnpackUShort(vbuf, wins);
            vbuf = nboUnpackUShort(vbuf, losses);
            vbuf = nboUnpackUShort(vbuf, tks);
            vbuf = nboUnpackString(vbuf, callsign, CallSignLen);
            vbuf = nboUnpackString(vbuf, motto, MottoLen);
            players[p].name = callsign;
            players[p].team = TeamColor(team);
            players[p].wins = wins;
            players[p].losses = losses;
            players[p].tks = tks;
            players[p].isRegistered = false;
            players[p].isVerified = false;
            players[p].isAdmin = false;
            if (ui != NULL)
                ui->addedPlayer(p);
            // If you are an admin, then MsgAdminInfo will output the message
            if (messageMask[MsgAddPlayer] && !players[getMyId()].isAdmin)
            {
                Team temp;
                std::string joinMsg = std::string("*** \'") + callsign + "\' joined the game as " +
                                      temp.getName(players[p].team) + ".";
                lastMessage.first = joinMsg;
            }
            break;

        case MsgRemovePlayer:
            vbuf = nboUnpackUByte(vbuf, p);
            if (ui != NULL)
                ui->removingPlayer(p);
            if (messageMask[MsgRemovePlayer])
            {
                lastMessage.first = std::string("*** '") + players[p].name +
                                    "' left the game.";
            }
            players.erase(p);
            break;

        case MsgPlayerInfo:
            uint8_t numPlayers;
            vbuf = nboUnpackUByte(vbuf, numPlayers);
            for (i = 0; i < numPlayers; ++i)
            {
                vbuf = nboUnpackUByte(vbuf, p);
                uint8_t info;
                // parse player info bitfield
                vbuf = nboUnpackUByte(vbuf, info);
                players[p].isAdmin = ((info & IsAdmin) != 0);
                players[p].isRegistered = ((info & IsRegistered) != 0);
                players[p].isVerified = ((info & IsVerified) != 0);
            }
            break;

        case MsgAdminInfo:
            uint8_t numIPs;
            uint8_t tmp;
            vbuf = nboUnpackUByte(vbuf, numIPs);
            if (numIPs > 1)
            {
                for (i = 0; i < numIPs; ++i)
                {
                    vbuf = nboUnpackUByte(vbuf, tmp);
                    vbuf = nboUnpackUByte(vbuf, p);
                    vbuf = a.unpack(vbuf);
                    players[p].ip = a.getDotNotation();
                    if ((ui != NULL) && messageMask[MsgAdminInfo])
                    {
                        ui->outputMessage("*** IPINFO: " + players[p].name + " from "  +
                                          players[p].ip, Default);
                    }
                }
            }
            //Alternative to the MsgAddPlayer message
            else if (numIPs == 1)
            {
                vbuf = nboUnpackUByte(vbuf, tmp);
                vbuf = nboUnpackUByte(vbuf, p);
                vbuf = a.unpack(vbuf);
                players[p].ip = a.getDotNotation();
                Team temp;
                if (messageMask[MsgAdminInfo])
                {
                    std::string joinMsg = std::string("*** \'") + players[p].name + "\' joined the game as " +
                                          temp.getName(players[p].team) + " from " + players[p].ip + ".";
                    lastMessage.first = joinMsg;
                }
            }
            break;

        case MsgScoreOver:
            if (messageMask[MsgScoreOver])
            {
                PlayerId id;
                uint16_t _team;
                vbuf = nboUnpackUByte(vbuf, id);
                vbuf = nboUnpackUShort(vbuf, _team);
                it = players.find(id);
                victimName = (it != players.end() ? it->second.name : "<unknown>");
                if (_team != (uint16_t)NoTeam)
                {
                    Team temp;
                    victimName = temp.getName((TeamColor)_team);
                }
                lastMessage.first = std::string("*** \'") + victimName + "\' won the game.";
            }
            break;

        case MsgTimeUpdate:
            if (messageMask[MsgTimeUpdate])
            {
                uint32_t timeLeft;
                vbuf = nboUnpackUInt(vbuf, timeLeft);
                if (timeLeft == 0)
                    lastMessage.first = "*** Time Expired.";
                else if (timeLeft == ~0u)
                    lastMessage.first = "*** Paused.";
                else
                    lastMessage.first = std::string("*** ") +
                                        TextUtils::format("%u", timeLeft) + " seconds remaining.";
            }
            break;

        case MsgKilled:
            if (messageMask[MsgKilled])
            {
                PlayerId victim, killer;
                FlagType* flagType;
                int16_t shotId, reason;
                vbuf = nboUnpackUByte(vbuf, victim);
                vbuf = nboUnpackUByte(vbuf, killer);
                vbuf = nboUnpackShort(vbuf, reason);
                vbuf = nboUnpackShort(vbuf, shotId);
                vbuf = FlagType::unpack(vbuf, flagType);
                if (reason == PhysicsDriverDeath)
                {
                    int32_t inPhyDrv;
                    vbuf = nboUnpackInt(vbuf, inPhyDrv);
                }

                // find the player names and build a kill message string
                it = players.find(victim);
                victimName = (it != players.end() ? it->second.name : "<unknown>");
                it = players.find(killer);
                killerName = (it != players.end() ? it->second.name : "<unknown>");
                lastMessage.first = std::string("*** ") + "'" + victimName + "' ";
                if (killer == victim)
                    lastMessage.first = lastMessage.first + "blew myself up.";
                else
                {
                    lastMessage.first = lastMessage.first + "destroyed by '" +
                                        killerName + "'.";
                }
            }
            break;

        case MsgSuperKill:
            return Superkilled;

        case MsgScore:
            uint8_t numScores;
            vbuf = nboUnpackUByte(vbuf, numScores);
            for (i = 0; i < numScores; i++)
            {
                uint16_t winners, loosers, teamkillers;
                vbuf = nboUnpackUByte(vbuf, p);
                vbuf = nboUnpackUShort(vbuf, winners);
                vbuf = nboUnpackUShort(vbuf, loosers);
                vbuf = nboUnpackUShort(vbuf, teamkillers);
                if ((iter = players.find(p)) != players.end())
                {
                    iter->second.wins   = winners;
                    iter->second.losses = loosers;
                    iter->second.tks    = teamkillers;
                }
            }
            if (messageMask[MsgScore])
            {
                lastMessage.first =
                    std::string("*** Received score update, score for ")+
                    TextUtils::format("%d", numScores) + " player" +
                    (numScores == 1 ? "s" : "") + " updated.";
            }
            break;

        case MsgMessage:

            // unpack the message header
            PlayerId src;
            PlayerId dst;
            uint8_t mtype;
            PlayerId me = sLink.getId();
            vbuf = nboUnpackUByte(vbuf, src);
            vbuf = nboUnpackUByte(vbuf, dst);
            vbuf = nboUnpackUByte(vbuf, mtype);

            // Only bother processing the message if we know how to handle it
            if (MessageType(mtype) != ChatMessage && MessageType(mtype) != ActionMessage)
                break;

            // format the message depending on src and dst
            TeamColor dstTeam = (LastRealPlayer < dst && dst <= FirstTeam ?
                                 TeamColor(FirstTeam - dst) : NoTeam);
            if (messageMask[MsgMessage])
            {
                lastMessage.first = formatMessage((const char*)vbuf, MessageType(mtype),
                                                  src, dst, dstTeam, me);
                PlayerIdMap::const_iterator iterator = players.find(src);
                lastMessage.second = (iterator == players.end() ?
                                      colorMap[NoTeam] :
                                      colorMap[iterator->second.team]);
            }
            break;
        }
        if (ui != NULL)
            ui->handleNewPacket(code);
        return GotMessage;
    }

    if (sLink.getState() != ServerLink::Okay)
    {
        if (ui != NULL)
            ui->outputMessage("--- ERROR: Communication error", Red);
        return CommError;
    }

    return NoMessage;
}


std::pair<std::string, ColorCode> BZAdminClient::getLastMessage() const
{
    return lastMessage;
}


PlayerIdMap& BZAdminClient::getPlayers()
{
    return players;
}


bool BZAdminClient::isValid() const
{
    return valid;
}

void BZAdminClient::outputServerList() const
{
    if (ui)
        ui->outputMessage(std::string("Server List:"), Yellow);
    ServerList serverList;

    serverList.startServerPings(&startupInfo);

    // wait no more than 20 seconds for the list server
    for (int i = 0; i < 20; i++)
    {
        if (!serverList.searchActive() && serverList.serverFound())
            break;
        if (ui)
        {
            if (!serverList.serverFound())
                ui->outputMessage(std::string("...waiting on the list server..."), Yellow);
            else
                ui->outputMessage(TextUtils::format("...retrieving list of servers... (found %zu)", serverList.size()), Yellow);
        }
        serverList.checkEchos(&startupInfo);
        cURLManager::perform();
        TimeKeeper::sleep(1.0);
    }
    // what is your final answer?
    serverList.checkEchos(&startupInfo);

    if (ui)
    {
        std::vector<ServerItem> servers = serverList.getServers();
        for (std::vector<ServerItem>::const_iterator server = servers.begin();
                server != servers.end();
                ++server)
            ui->outputMessage(std::string("  ") + server->description, Yellow);
        ui->outputMessage(std::string("End Server List."), Yellow);
    }

    return;
}

void BZAdminClient::runLoop()
{
    std::string cmd;
    ServerCode what(NoMessage);
    while (true)
    {
        what = checkMessage();
        if (what == Superkilled || what == CommError)
            break;
        if (ui != NULL && ui->checkCommand(cmd))
        {
            if (cmd == "/quit")
                break;
            else if (cmd.substr(0, 6) == "/show ")
            {
                if (msgTypeMap.find(cmd.substr(6)) == msgTypeMap.end())
                {
                    ui->outputMessage(std::string("--- ERROR: ") + cmd.substr(6) +
                                      " is an unknown message type", Red);
                }
                else
                {
                    showMessageType(cmd.substr(6));
                    ui->outputMessage(std::string("--- Will now show messages of the ")
                                      + "type " + cmd.substr(6), Yellow);
                }
            }
            else if (cmd.substr(0, 6) == "/hide ")
            {
                if (msgTypeMap.find(cmd.substr(6)) == msgTypeMap.end())
                {
                    ui->outputMessage(std::string("--- ERROR: ") + cmd.substr(6) +
                                      " is an unknown message type", Red);
                }
                else
                {
                    ignoreMessageType(cmd.substr(6));
                    ui->outputMessage(std::string("--- Will now hide messages of the ")
                                      + "type " + cmd.substr(6), Yellow);
                }
            }
            else if (cmd == "/list")
                outputServerList();
            else if (cmd != "")
                sendMessage(cmd, ui->getTarget());
        }
    }

    // why did we leave the loop?
    switch (what)
    {
    case Superkilled:
        lastMessage.first = "--- ERROR: Server forced disconnect";
        lastMessage.second = Red;
        break;
    case CommError:
        lastMessage.first = "--- ERROR: Connection to server lost";
        lastMessage.second = Red;
        break;
    default:
        waitForServer();
    }
}


void BZAdminClient::sendMessage(const std::string& msg,
                                PlayerId target)
{
    // local commands:
    // /set lists all BZDB variables
    if (msg == "/set")
    {
        if (ui != NULL)
            BZDB.iterate(listSetVars, this);
        return;
    }

    // Flawfinder: ignore
    char buffer[MessageLen];
    // Flawfinder: ignore
    char buffer2[1 + MessageLen];
    void* buf = buffer2;

    buf = nboPackUByte(buf, target);
    // Flawfinder: ignore
    strncpy(buffer, msg.c_str(), MessageLen - 1);
    buffer[MessageLen - 1] = '\0';
    nboPackString(buffer2 + 1, buffer, MessageLen);
    sLink.send(MsgMessage, sizeof(buffer2), buffer2);
}


std::string BZAdminClient::formatMessage(const std::string& msg,
        const MessageType type, PlayerId src,
        PlayerId dst, TeamColor dstTeam,
        PlayerId me)
{
    std::string formatted = "    ";

    // get sender and receiver
    const std::string srcName = (src == ServerPlayer ? "SERVER" :
                                 (players.count(src) ? players[src].name :
                                  "(UNKNOWN)"));
    const std::string dstName = (players.count(dst) ? players[dst].name :
                                 "(UNKNOWN)");

    // direct message to or from me
    if (dst == me || players.count(dst))
    {
        if (!(src == me && dst == me))
        {
            if (src == me)
            {
                if (type == ActionMessage)
                    formatted += "[->" + dstName + "][" + srcName + " " + msg + "]";
                else
                    formatted += "[->" + dstName + "] " + msg;
            }
            else
            {
                if (type == ActionMessage)
                    formatted += "[" + srcName + " " + msg + "]";
                else
                    formatted += "[" + srcName + "->] " + msg;
            }
        }
        else
            formatted += msg;
    }

    // public or admin or team message
    else
    {
        if (dst == AdminPlayers)
            formatted += "[Admin] ";
        else if (dstTeam != NoTeam)
            formatted += "[Team] ";

        formatted += srcName;
        if (type != ActionMessage)
            formatted += ":";
        formatted += " ";
        formatted += msg;
    }

    return formatted;
}


void BZAdminClient::setUI(BZAdminUI* bzInterface)
{
    ui = bzInterface;
}


void BZAdminClient::waitForServer()
{
    // we need to know that the server has processed all our messages
    // send a private message to ourself and wait for it to come back
    // this assumes that the order of messages isn't changed along the way
    bool tmp = messageMask[MsgMessage];
    messageMask[MsgMessage] = true;
    PlayerId me = sLink.getId();
    if (sLink.getState() == ServerLink::Okay)
    {
        sendMessage("bzadminping", me);
        std::string expected = formatMessage("bzadminping", ChatMessage, me, me, NoTeam, me);
        std::string noTalk = formatMessage("We're sorry, you are not allowed to talk!", ChatMessage, ServerPlayer, me, NoTeam,
                                           me);
        BZAdminUI* tmpUI = ui;
        ui = NULL;
        do
        {
            checkMessage();
        }
        while (lastMessage.first != expected && lastMessage.first != noTalk);
        ui = tmpUI;
    }
    messageMask[MsgMessage] = tmp;
}


void BZAdminClient::ignoreMessageType(uint16_t type)
{
    messageMask[type] = false;
}


void BZAdminClient::showMessageType(uint16_t type)
{
    messageMask[type] = true;
}


void BZAdminClient::ignoreMessageType(std::string type)
{
    ignoreMessageType(msgTypeMap[type]);
}


void BZAdminClient::showMessageType(std::string type)
{
    showMessageType(msgTypeMap[type]);
}


void BZAdminClient::listSetVars(const std::string& name, void* thisObject)
{
    //Flawfinder: ignore
    char message[MessageLen];
    if (BZDB.getPermission(name) == StateDatabase::Locked)
    {
        // Flawfinder: ignore
        snprintf(message, sizeof(message), "/set %s %f", name.c_str(), BZDB.eval(name));
        ((BZAdminClient*)thisObject)->ui->outputMessage(message, Default);
    }
}


const std::map<std::string, uint16_t>& BZAdminClient::getMessageTypeMap() const
{
    return msgTypeMap;
}

bool BZAdminClient::getFilterStatus(uint16_t msgType) const
{
    std::map<uint16_t, bool>::const_iterator iter = messageMask.find(msgType);
    if (iter == messageMask.end())
        return false;
    else
        return iter->second;
}


// Local Variables: ***
// mode: C++ ***
// tab-width: 4 ***
// c-basic-offset: 4 ***
// indent-tabs-mode: nil ***
// End: ***
// ex: shiftwidth=4 tabstop=4
