/*
 * AuthentServer.cpp by Keith Fulton <keith@paqrat.com>
 *
 * Copyright (C) 2001 Atomic Blue (info@planeshift.it, http://www.atomicblue.org) 
 *
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation (version 2 of the License)
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */
#include <psconfig.h>
#include <physicallayer/entity.h>
#include <propclass/mesh.h>

#include <iutil/cfgmgr.h>
#include <csutil/csmd5.h>

#include "authentserver.h"
#include "adminmanager.h"
#include "util/psdatabase.h"

#include "weathermanager.h"
#include "net/messages.h"
#include "net/msghandler.h"
#include "client.h"
#include "playergroup.h"
#include "gem.h"
#include "bulkobjects/psaccountinfo.h"
#include "bulkobjects/psguildinfo.h"
#include "bulkobjects/pscharacterlist.h"
#include "clients.h"
//#include "util/serverconsole.h"
#include "util/log.h"
#include "psserver.h"
#include "events.h"
#include "cachemanager.h"
#include "bulkobjects/pscharacterloader.h"
#include "bulkobjects/pscharacter.h"
#include "globals.h"
#include "util/eventmanager.h"
#include "icachedobject.h"

class CachedAuthMessage : public iCachedObject
{
public: 
    psAuthApprovedMessage *msg;

    CachedAuthMessage(psAuthApprovedMessage *message) { msg = message; }
    ~CachedAuthMessage() { delete msg; }

    // iCachedObject Functions below
    virtual void ProcessCacheTimeout() {};          /// required for iCachedObject but not used here
    virtual void *RecoverObject() { return this; }  /// Turn iCachedObject ptr into psAccountInfo
    virtual void DeleteSelf()     { delete this; }  /// Delete must come from inside object to handle operator::delete overrides.
};


psAuthenticationServer::psAuthenticationServer(ClientConnectionSet *pCCS,
                                               UserManager *usermgr,
                                               GuildManager *gm)
{
    clients      = pCCS;
    usermanager  = usermgr;
    guildmanager = gm;
    msgstringsmessage = NULL;

    psserver->GetEventManager()->Subscribe(this,MSGTYPE_PREAUTHENTICATE,REQUIRE_ANY_CLIENT);
    psserver->GetEventManager()->Subscribe(this,MSGTYPE_AUTHENTICATE,REQUIRE_ANY_CLIENT);
    psserver->GetEventManager()->Subscribe(this,MSGTYPE_DISCONNECT,REQUIRE_ANY_CLIENT);
    psserver->GetEventManager()->Subscribe(this,MSGTYPE_AUTHCHARACTER,REQUIRE_ANY_CLIENT);
    psserver->GetEventManager()->Subscribe(this,MSGTYPE_SYSTEM,REQUIRE_ANY_CLIENT); // Handle the heartbeat from the client
    psserver->GetEventManager()->Subscribe(this,MSGTYPE_CLIENTSTATUS,REQUIRE_ANY_CLIENT); // Handle the heartbeat from the client
}

psAuthenticationServer::~psAuthenticationServer()
{
    if (psserver->GetEventManager())
    {
        psserver->GetEventManager()->Unsubscribe(this,MSGTYPE_PREAUTHENTICATE);
        psserver->GetEventManager()->Unsubscribe(this,MSGTYPE_AUTHENTICATE);
        psserver->GetEventManager()->Unsubscribe(this,MSGTYPE_DISCONNECT);
        psserver->GetEventManager()->Unsubscribe(this,MSGTYPE_AUTHCHARACTER);
        psserver->GetEventManager()->Unsubscribe(this,MSGTYPE_SYSTEM);
        psserver->GetEventManager()->Unsubscribe(this,MSGTYPE_CLIENTSTATUS);
    }
    if (msgstringsmessage)
        delete msgstringsmessage;
}

void psAuthenticationServer::HandleMessage(MsgEntry *me,Client *client)
{
    if (me == NULL) 
    {
        Bug1("No Message Entity Found");
        return;
    }

    switch (me->GetType())
    {
        case MSGTYPE_AUTHCHARACTER:
            HandleAuthCharacter( me );
            break;
            
        case MSGTYPE_AUTHENTICATE:
            HandleAuthent(me);
            break;

        case MSGTYPE_PREAUTHENTICATE:
            HandlePreAuthent(me);
            break;

        case MSGTYPE_DISCONNECT:
            HandleDisconnect(me, "Your client has disconnected. If you are seeing this message a connection error has likely occurred.");
            break;

        case MSGTYPE_CLIENTSTATUS:
            HandleStatusUpdate(me, client);
            break;
    }
}

void psAuthenticationServer::HandleAuthCharacter( MsgEntry* me )
{
    Client *client = clients->FindAny(me->clientnum);
    if (!client)
    {
        Bug2("Couldn't find client %d?!?",me->clientnum);
        return;
    }

    psCharacterPickerMessage charpick( me );
    if (!charpick.valid)
    {
        Debug1(LOG_NET,me->clientnum,"Mangled psCharacterPickerMessage received.\n");
        return;
    } 

    psCharacterList *charlist = psserver->CharacterLoader.LoadCharacterList( client->GetAccountID());
    
    if (!charlist)
    {
        Error1("Could not load Character List for account! Rejecting client!\n");
        psserver->RemovePlayer( me->clientnum, "Could not load the list of characters for your account.  Please contact a PS Admin for help.");        
        return;
    } 

    int i;
    for (i=0;i<MAX_CHARACTERS_IN_LIST;i++)
    {
        if (charlist->GetEntryValid(i))
        {
            // Trim out whitespaces from name
            charpick.characterName.Trim();
            csString listName( charlist->GetCharacterFullName(i) );
            listName.Trim();
                                            
            if ( charpick.characterName == listName )
            {
                 client->SetPlayerID(charlist->GetCharacterID(i));
                 // Set client name in code to just firstname as other code depends on it
                 client->SetName(charlist->GetCharacterName(i));
                 psCharacterApprovedMessage out( me->clientnum );
                 out.SendMessage();
                 break;
            }                     
        }
    }

    // cache will auto-delete this ptr if it times out
    CacheManager::GetSingleton().AddToCache(charlist, CacheManager::GetSingleton().MakeCacheName("list",client->GetAccountID()),120);
}


bool psAuthenticationServer::CheckAuthenticationPreCondition(int clientnum, bool netversionok, const char * sUser)
{
    /**
     * CHECK 1: Is Network protokol compatible?
     */
    if (!netversionok)
    {
        psserver->RemovePlayer (clientnum, "You are not running the correct version of Planeshift. Please launch the updater.");
        return false;
    }

    /**
     * CHECK 2: Server has loaded a map
     */

    // check if server is already ready (ie level loaded)
    if (!psserver->IsReady())
    {
        if (psserver->HasBeenReady())
        {
            // Locked
            psserver->RemovePlayer(clientnum,"The server is up but about to shutdown. Please try again in 2 minutes.");

            Notify2(LOG_CONNECTIONS,"User '%s' authentication request rejected: Server does not accept connections anymore.", sUser );
        }
        else
        {
            // Not ready yet
            psserver->RemovePlayer(clientnum,"The server is up but not fully ready to go yet. Please try again in a few minutes.");

            Notify2(LOG_CONNECTIONS,"User '%s' authentication request rejected: Server has not loaded a world map.\n", sUser );
        }
        
        return false;
    }

    return true;
}


void psAuthenticationServer::HandlePreAuthent(MsgEntry *me)
{
    psPreAuthenticationMessage msg(me);
    if (!msg.valid)
    {
        Debug1(LOG_NET,me->clientnum,"Mangled psPreAuthenticationMessage received.\n");
        return;
    }

    if (!CheckAuthenticationPreCondition(me->clientnum,msg.NetVersionOk(),"pre"))
        return;
    
    psPreAuthApprovedMessage reply(me->clientnum);
    reply.SendMessage();
}

void psAuthenticationServer::HandleAuthent(MsgEntry *me)
{
    csTicks start = csGetTicks();

    psAuthenticationMessage msg(me); // This cracks message into members.

    if (!msg.valid)
    {
        Debug1(LOG_NET,me->clientnum,"Mangled psAuthenticationMessage received.\n");
        return;
    }

    if (!CheckAuthenticationPreCondition(me->clientnum, msg.NetVersionOk(),msg.sUser))
        return;

    csString status;
    status.Format("%s, %u, Received Authentication Message", (const char *) msg.sUser, me->clientnum);
    psserver->GetLogCSV()->Write(CSV_AUTHENT, status);

    if ( msg.sUser.Length() == 0 || msg.sPassword.Length() == 0)
    {
        psserver->RemovePlayer(me->clientnum,"No username or password entered");

        Notify2(LOG_CONNECTIONS,"User '%s' authentication request rejected: No username or password.\n",
                (const char *)msg.sUser);            
        return;                
    }
    
    // Check if login was correct
    Notify2(LOG_CONNECTIONS,"Check Login for: '%s'\n", (const char*)msg.sUser);
    psAccountInfo *acctinfo=CacheManager::GetSingleton().GetAccountInfoByUsername((const char *)msg.sUser);

    if ( !acctinfo )
    {
        // invalid
        psserver->RemovePlayer(me->clientnum,"No account found with that name");

        Notify2(LOG_CONNECTIONS,"User '%s' authentication request rejected: No account found with that name.\n",
                (const char *)msg.sUser);            
        return;                
    }

    // Add account to cache to optimize repeated login attempts
    CacheManager::GetSingleton().AddToCache(acctinfo,msg.sUser,120);

    /**
     * Check if the client is already logged in
     */
    Client* existingClient = clients->FindAccount(acctinfo->accountid);
    if (existingClient)  // account already logged in
    {
        // invalid authent message from a different client
        if(existingClient->GetClientNum() != me->clientnum)
        {
            csString reason;
            if(existingClient->zombie)
                reason.Format("Your character(%s) was still in combat or casting a spell when you disconnected. Please wait for combat to finish and try again.", existingClient->GetName());
            else
                reason.Format("You are already logged on to this server as %s. If you were disconnected, please wait 30 seconds and try again.", existingClient->GetName());

            psserver->RemovePlayer(me->clientnum, reason);
            Notify2(LOG_CONNECTIONS,"User '%s' authentication request rejected: User already logged in.\n",
                (const char *)msg.sUser);
        }

        // No delete necessary because AddToCache will auto-delete
        // delete acctinfo;
        return;
    }

    csString passwordhashandclientnum (acctinfo->password);
    passwordhashandclientnum.Append(":");
    passwordhashandclientnum.Append(me->clientnum);
    
    csString encoded_hash = csMD5::Encode(passwordhashandclientnum).HexString();
    if (acctinfo==NULL || strcmp( encoded_hash.GetData() ,
                                  msg.sPassword.GetData())) // authentication error
    {
        psserver->RemovePlayer(me->clientnum, "Incorrect password.");

        if (acctinfo==NULL)
        {
            Notify2(LOG_CONNECTIONS,"User '%s' authentication request rejected (Username not found).",(const char *)msg.sUser);
        }
        else
        {
            Notify2(LOG_CONNECTIONS,"User '%s' authentication request rejected (Bad password).",(const char *)msg.sUser);
        }
        // No delete necessary because AddToCache will auto-delete
        // delete acctinfo;
        return;
    }

    if(csGetTicks() - start > 500)
    {
        csString status;
        status.Format("Warning: Spent %u time authenticating account ID %u, After password check", 
            csGetTicks() - start, acctinfo->accountid);
        psserver->GetLogCSV()->Write(CSV_STATUS, status);
    }


    Client *client = clients->FindAny(me->clientnum);
    if (!client)
    {
        Bug2("Couldn't find client %d?!?",me->clientnum);
        // No delete necessary because AddToCache will auto-delete
        // delete acctinfo;
        return;
    }

    client->SetName(msg.sUser);
    client->SetAccountID( acctinfo->accountid );
    

    // Check to see if the client is banned
    time_t now = time(0);
    BanEntry* ban = banmanager.GetBanByAccount(acctinfo->accountid);
    if (ban == NULL)
    {
        // Account not banned; try IP range
        ban = banmanager.GetBanByIPRange(client->GetIPRange());
        if (ban && ban->end && now > ban->start + IP_RANGE_BAN_TIME)
        {  
            // Only ban by IP range for the first 2 days
            ban = NULL;
        }
    }
    if (ban)
    {
        if (now > ban->end)  // Time served
        {
            banmanager.RemoveBan(acctinfo->accountid);
        }
        else  // Notify and block
        {
            tm* timeinfo = localtime(&(ban->end));
            csString banmsg;
            banmsg.Format("You are banned until %d-%d-%d %d:%d.  Reason: %s",
                          timeinfo->tm_year+1900,
                          timeinfo->tm_mon+1,
                          timeinfo->tm_mday,
                          timeinfo->tm_hour,
                          timeinfo->tm_min,
                          ban->reason.GetData() );
    
            psserver->RemovePlayer(me->clientnum, banmsg);
    
            Notify2(LOG_CONNECTIONS,"User '%s' authentication request rejected (Banned).",(const char *)msg.sUser);
            // No delete necessary because AddToCache will auto-delete
            // delete acctinfo;
            return;
        }
    }

    if(csGetTicks() - start > 500)
    {
        csString status;
        status.Format("Warning: Spent %u time authenticating account ID %u, After ban check", 
            csGetTicks() - start, acctinfo->accountid);
        psserver->GetLogCSV()->Write(CSV_STATUS, status);
    }

    /** Check to see if there are any players on that account.  All accounts should have
    *    at least one player in this function.
    */
    psCharacterList *charlist = psserver->CharacterLoader.LoadCharacterList(acctinfo->accountid);

    if (!charlist)
    {
        Error2("Could not load Character List for account! Rejecting client %s!\n",(const char *)msg.sUser);
        psserver->RemovePlayer( me->clientnum, "Could not load the list of characters for your account.  Please contact a PS Admin for help.");
        delete acctinfo;
        return;
    }

    // cache will auto-delete this ptr if it times out
    CacheManager::GetSingleton().AddToCache(charlist, CacheManager::GetSingleton().MakeCacheName("list",client->GetAccountID()),120);

    
     /**
     * CHECK 6: Connection limit
     * 
     * We check against number of concurrent connections, but players with
     * security rank of GameMaster or higher are not subject to this limit.
     */
    if (psserver->IsFull(clients->Count(),client)) 
    {
        // invalid
        psserver->RemovePlayer(me->clientnum, "The server is full right now.  Please try again in a few minutes.");

        Notify2(LOG_CONNECTIONS, "User '%s' authentication request rejected: Too many connections.\n", (const char *)msg.sUser );
        // No delete necessary because AddToCache will auto-delete
        // delete acctinfo;
        status = "User limit hit!";
        psserver->GetLogCSV()->Write(CSV_STATUS, status);
        return;
    }

    Notify3(LOG_CONNECTIONS,"User '%s' (%d) added to active client list\n",(const char*) msg.sUser, me->clientnum);

    // Get the struct to refresh
    // Update last login ip and time
    char addr[20];
    client->GetIPAddress(addr);
    acctinfo->lastloginip = addr;

    tm* localtm = localtime(&now);
    csString timeStr;
    timeStr.Format("%d-%02d-%02d %02d:%02d:%02d",
        localtm->tm_year+1900,
        localtm->tm_mon+1,
        localtm->tm_mday,
        localtm->tm_hour,
        localtm->tm_min,
        localtm->tm_sec);

    acctinfo->lastlogintime = timeStr;
    CacheManager::GetSingleton().UpdateAccountInfo(acctinfo);

    iCachedObject *obj = CacheManager::GetSingleton().RemoveFromCache(CacheManager::GetSingleton().MakeCacheName("auth",acctinfo->accountid));
    CachedAuthMessage *cam;

    if (!obj)
    {
        // Send approval message
        psAuthApprovedMessage *message = new psAuthApprovedMessage(me->clientnum,client->GetPlayerID(), charlist->GetValidCount() );    

        if(csGetTicks() - start > 500)
        {
            csString status;
            status.Format("Warning: Spent %u time authenticating account ID %u, After approval", 
                csGetTicks() - start, acctinfo->accountid);
            psserver->GetLogCSV()->Write(CSV_STATUS, status);
        }

        // Send out the character list to the auth'd player    
        for (int i=0; i<MAX_CHARACTERS_IN_LIST; i++)
        {
            if (charlist->GetEntryValid(i))
            {
                // Quick load the characters to get enough info to send to the client
                psCharacter* character = psserver->CharacterLoader.QuickLoadCharacterData( charlist->GetCharacterID(i) );
                if (character == NULL)
                {
                    Error2("QuickLoadCharacterData failed for character '%s'", charlist->GetCharacterName(i));
                    continue;
                }

                Notify3(LOG_CHARACTER, "Sending %s to client %d\n", character->name.GetData(), me->clientnum );
                character->AppendCharacterSelectData(*message);

                delete character;
            }
        }
        message->msg->ClipToCurrentSize();
        cam = new CachedAuthMessage(message);
    }
    else
    {
        // recover underlying object
        cam = (CachedAuthMessage *)obj->RecoverObject();
        // update client id since new connection here
        cam->msg->msg->clientnum = me->clientnum;
    }
    // Send auth approved and char list in one message now
    cam->msg->SendMessage();
    CacheManager::GetSingleton().AddToCache(cam,CacheManager::GetSingleton().MakeCacheName("auth",acctinfo->accountid),120);

    SendMsgStrings(me->clientnum); 
    
    client->SetSpamPoints(acctinfo->spamPoints);
    client->SetAdvisorPoints(acctinfo->advisorPoints);
    client->SetSecurityLevel(acctinfo->securitylevel);

    psserver->GetWeatherManager()->SendClientCurrentTime(me->clientnum);

    SendUpdateInfo(me->clientnum);

    if(csGetTicks() - start > 500)
    {
        csString status;
        status.Format("Warning: Spent %u time authenticating account ID %u, After load", 
            csGetTicks() - start, acctinfo->accountid);
        psserver->GetLogCSV()->Write(CSV_STATUS, status);
    }

    status.Format("%s - %s, %u, Logged in", addr, (const char*) msg.sUser, me->clientnum);
    psserver->GetLogCSV()->Write(CSV_AUTHENT, status);
}

void psAuthenticationServer::HandleDisconnect(MsgEntry* me,const char *msg)
{
    psDisconnectMessage mesg(me);
    
    Client *client = clients->FindAny(me->clientnum);

    if(!client->AllowDisconnect())
        return;

    if(mesg.msgReason == "!")
        psserver->RemovePlayer(me->clientnum,"!");
    else
        psserver->RemovePlayer(me->clientnum,msg);
}

void psAuthenticationServer::SendDisconnect(Client* client, const char *reason)
{
    if (client->GetActor())
    {
        psDisconnectMessage msg(client->GetClientNum(), client->GetActor()->GetEntity()->GetID(),reason);
        if (msg.valid)
        {
            msg.msg->priority = PRIORITY_LOW;
            psserver->GetEventManager()->Broadcast(msg.msg, NetBase::BC_FINALPACKET);
        }
        else
        {
            Bug2("Could not create a valid psDisconnectMessage for client %u.\n",client->GetClientNum());
        }
    }
    else
    {
        psDisconnectMessage msg(client->GetClientNum(), 0, reason);
        if (msg.valid)
            psserver->GetEventManager()->Broadcast(msg.msg, NetBase::BC_FINALPACKET);
    }
}

void psAuthenticationServer::SendMsgStrings(int cnum)
{
    // send message strings hash table to client
    if (!msgstringsmessage)
        msgstringsmessage = new psMsgStringsMessage(cnum, CacheManager::GetSingleton().GetMsgStrings());
    else
        msgstringsmessage->msg->clientnum = cnum;
    if (!msgstringsmessage->valid)
    {
        Bug1("Could not form a valid psMsgStringsMessage from Message Strings!\n");
        delete msgstringsmessage;
        msgstringsmessage=NULL;
    }
    else
        msgstringsmessage->SendMessage();
}

void psAuthenticationServer::HandleStatusUpdate(MsgEntry* me, Client* client)
{
    psClientStatusMessage msg(me);
    // printf("Got ClientStatus message!\n");

    // We ignore !ready messages because of abuse potential.
    if (msg.ready)
    {
        psConnectEvent evt(client->GetClientNum());
        evt.FireEvent();
        client->SetReady(msg.ready);
    }
}

void psAuthenticationServer::SendUpdateInfo(unsigned int cnum)
{
    unsigned int timestamp = CacheManager::GetSingleton().GetUpdateTimeStamp();
    if (cnum && timestamp)
    {
        psUpdateInfo info(cnum,timestamp);
        info.SendMessage();
    }
}

BanManager::BanManager()
{
    Result result(db->Select("SELECT * FROM bans"));
    if (!result.IsValid())
        return;

    time_t now = time(0);
    for (unsigned int i=0; i<result.Count(); i++)
    {
        uint32 account = result[i].GetUInt32("account");
        time_t end = result[i].GetUInt32("end");

        if (now > end)  // Time served
        {
            db->Command("DELETE FROM bans WHERE account='%u'", account );
            continue;
        }

        BanEntry* newentry = new BanEntry;
        newentry->account = account;
        newentry->end = end;
        newentry->start = result[i].GetUInt32("start");
        newentry->ip_range = result[i]["ip_range"];
        newentry->reason = result[i]["reason"];
        
        // If account ban, add to list
        if (newentry->account)
            banList_IDHash.Put(newentry->account,newentry);

        // If IP range ban, add to list
        if ( newentry->ip_range.Length() && (!end || now < newentry->start + IP_RANGE_BAN_TIME) )
            banList_IPRList.Push(newentry);
    }
}

BanManager::~BanManager()
{
    csHash<BanEntry*,uint32>::GlobalIterator it(banList_IDHash.GetIterator());
    while (it.HasNext())
    {
        BanEntry* entry = it.Next();
        delete entry;
    }
}

bool BanManager::RemoveBan(uint32 account)
{
    BanEntry* ban = GetBanByAccount(account);
    if (!ban)
        return false;  // Not banned

    db->Command("DELETE FROM bans WHERE account='%u'", account );
    banList_IDHash.Delete(account,ban);
    banList_IPRList.Delete(ban);  // Deletes ban object
    return true;
}

bool BanManager::AddBan(uint32 account, csString ipRange, time_t duration, csString reason)
{
    if (GetBanByAccount(account))
        return false;  // Already banned

    time_t now = time(0);

    BanEntry* newentry = new BanEntry;
    newentry->account = account;
    newentry->start = now;
    newentry->end = now + duration;
    newentry->ip_range = ipRange;
    newentry->reason = reason;

    int ret = db->Command("INSERT INTO bans "
                          "(account,ip_range,start,end,reason) "
                          "VALUES ('%u','%s','%u','%u','%s')",
                          newentry->account,
                          newentry->ip_range.GetData(),
                          newentry->start,
                          newentry->end,
                          newentry->reason.GetData() );
    if (ret == -1)
    {
        delete newentry;
        return false;
    }

    if (newentry->account)
        banList_IDHash.Put(newentry->account,newentry);

    if (newentry->ip_range.Length())
        banList_IPRList.Push(newentry);

    return true;
}

BanEntry* BanManager::GetBanByAccount(uint32 account)
{
    return banList_IDHash.Get(account,NULL);
}

BanEntry* BanManager::GetBanByIPRange(csString IPRange)
{
    for (size_t i=0; i<banList_IPRList.Length(); i++)
        if ( IPRange.StartsWith(banList_IPRList[i]->ip_range) )
            return banList_IPRList[i];
    return NULL;
}
