// ACR's simplified auth

uint nextauthreq = 0; // move down?

client *findauth(uint id)
{
    loopv(clients) if(clients[i]->authreq == id) return clients[i];
    return NULL;
}

extern vector<authrequest> authrequests;

inline bool canreqauth(client &cl, int authtoken, int authuser)
{
    extern bool canreachauthserv;
    if (!isdedicated || !canreachauthserv){ sendf(&cl, 1, "ri2", SV_AUTH_ACR_CHAL, 2); return false; } // not dedicated/connected
    // if (ci.authreq){ sendf(&cl, 1, "ri2", SV_AUTH_ACR_CHAL, 1); return false; } // already pending
    // if (ci.authmillis + 2000 > servmillis){ sendf(&cl, 1, "ri3", SV_AUTH_ACR_CHAL, 6, cl.authmillis + 2000 - servmillis); return false; } // flood check
    return true;
}

int allowconnect(client &cl, uint authreq = 0, uint authuser = 0)
{
    if (cl.type == ST_LOCAL) return DISC_NONE;
    //if (!m_valid(gamemode)) return DISC_PRIVATE;
    if (cl.role >= CR_ADMIN) return DISC_NONE;

    if (authreq && authuser && canreqauth(cl, authreq, authuser))
    {
        cl.authtoken = authreq;
        cl.authuser = authuser;
        if (!nextauthreq)
            nextauthreq = 1;
        cl.authreq = nextauthreq++;
        cl.connectauth = true;
        logline(ACLOG_INFO, "[%s] %s logged in, requesting auth #%u as %u", cl.gethostname(), cl.formatname(), cl.authreq, authuser);
        return DISC_NONE;
    }

    int bantype = getbantype(cl);
    bool banned = bantype > BAN_NONE;
    bool srvfull = numnonlocalclients() > scl.maxclients;
    bool srvprivate = mastermode == MM_PRIVATE || mastermode == MM_MATCH;
    bool matchreconnect = mastermode == MM_MATCH && findscore(cl, false);
    int bl = 0, wl = nickblacklist.checkwhitelist(cl);
    const char *wlp = wl == NWL_PASS ? ", nickname whitelist match" : "";
    pwddetail pd;
    if (wl == NWL_UNLISTED) bl = nickblacklist.checkblacklist(cl.name);
    if (matchreconnect && !banned)
    {
        // former player reconnecting to a server in match mode
        logline(ACLOG_INFO, "[%s] %s logged in (reconnect to match)%s", cl.gethostname(), cl.formatname(), wlp);
        return DISC_NONE;
    }
    else if (wl == NWL_IPFAIL || wl == NWL_PWDFAIL)
    {
        // nickname matches whitelist, but IP is not in the required range or PWD doesn't match
        logline(ACLOG_INFO, "[%s] '%s' matches nickname whitelist: wrong %s%s", cl.gethostname(), cl.formatname(), wl == NWL_IPFAIL ? "IP" : "PWD", wlp);
        return wl == NWL_IPFAIL ? DISC_NAME_IP : DISC_NAME_PWD;
    }
    else if (bl > 0)
    {
        // nickname matches blacklist
        logline(ACLOG_INFO, "[%s] '%s' matches nickname blacklist line %d%s", cl.gethostname(), cl.formatname(), bl, wlp);
        return DISC_NAME;
    }
    else if (passwords.check(cl.name, cl.pwd, cl.salt, &pd, (cl.type == ST_TCPIP ? cl.peer->address.host : 0)) && (pd.priv >= CR_ADMIN || (banned && !srvfull && !srvprivate))) // pass admins always through
    {
        // admin (or deban) password match
        bool banremoved = false;
        if (pd.priv) setpriv(cl, pd.priv);
        if (bantype == BAN_VOTE)
        {
            loopv(bans) if (bans[i].address.host == cl.peer->address.host) { bans.remove(i); banremoved = true; break; } // remove admin bans
        }
        logline(ACLOG_INFO, "[%s] %s logged in using the admin password in line %d%s%s", cl.gethostname(), cl.formatname(), pd.line, wlp, banremoved ? ", ban removed" : "");
        return DISC_NONE;
    }
    else if (scl.serverpassword[0] && !(srvprivate || srvfull || banned))
    {
        // server password required
        if (!strcmp(genpwdhash(cl.name, scl.serverpassword, cl.salt), cl.pwd))
        {
            logline(ACLOG_INFO, "[%s] %s client logged in (using serverpassword)%s", cl.gethostname(), cl.formatname(), wlp);
            return DISC_NONE;
        }
        else return DISC_WRONGPW;
    }
    else if (srvprivate) return DISC_MASTERMODE;
    else if (srvfull) return DISC_MAXCLIENTS;
    else if (banned) return DISC_BANREFUSE;
    // does the master server want a disconnection?
    else if (!scl.bypassglobalbans && cl.authpriv == -1 && cl.masterdisc) return cl.masterdisc;
    else
    {
        logline(ACLOG_INFO, "[%s] %s logged in (default)%s", cl.gethostname(), cl.formatname(), wlp);
        return DISC_NONE;
    }
}

void checkauthdisc(client &cl, bool force = false)
{
    if (cl.connectauth || force)
    {
        cl.connectauth = false;
        const int disc = allowconnect(cl);
        if (disc) disconnect_client(cl, disc);
    }
}

void authfailed(uint id, bool fail)
{
    client *cl = findauth(id);
    if(!cl) return;
    cl->authreq = 0;
    logline(ACLOG_INFO, "[%s] auth #%u %s!", cl->gethostname(), id, fail ? "failed" : "had an error");
    sendf(cl, 1, "ri2", SV_AUTH_ACR_CHAL, 3);
    checkauthdisc(*cl);
}

void authsucceeded(uint id, int priv, const char *name)
{
    client *cl = findauth(id);
    if (!cl) return;
    cl->authreq = 0;
    filtertext(cl->authname, name);
    if (scl.bypassglobalpriv && priv != -1)
    {
        priv = CR_DEFAULT;
        logline(ACLOG_INFO, "[%s] auth #%u succeeded as '%s' (%s ignored)", cl->gethostname(), id, cl->authname, privname(priv));
    }
    else
        logline(ACLOG_INFO, "[%s] auth #%u succeeded for %s as '%s'", cl->gethostname(), id, privname(priv, true), cl->authname);
    sendf(NULL, 1, "ri4s", SV_AUTH_ACR_CHAL, 5, cl->clientnum, priv, cl->authname);
    if (priv < 0)
    {
        // name only
        cl->authpriv = -2;
    }
    else
    {
        // even CR_DEFAULT can bypass master bans
        // remove temporary bans
        loopv(bans)
            if (bans[i].type == BAN_VOTE && bans[i].address.host == cl->peer->address.host)
                bans.remove(i--);
        cl->authpriv = clamp(priv, (int)CR_DEFAULT, (int)CR_MAX);
        // setpriv(*cl, cl->authpriv);
        // unmute if auth has privilege
        // cl->muted = false;
    }
    checkauthdisc(*cl); // can bypass passwords
}

void authchallenged(uint id, const char *chal)
{
    client *cl = findauth(id);
    if(!cl) return;
    uchar buf[128] = { 0 };
    loopi(128)
    {
        #define hex2char(c) (c > '9' ? c-'a'+10 : c-'0')
        if(!chal[i << 1])
            break;
        buf[i] = hex2char(chal[i << 1]) << 4;
        if(!chal[(i << 1) | 1])
            break;
        buf[i] |= hex2char(chal[(i << 1) | 1]);
    }
    sendf(cl, 1, "ri2m", SV_AUTH_ACR_REQ, cl->authtoken, 128, buf);
    logline(ACLOG_INFO, "[%s] auth #%u challenged by master", cl->gethostname(), id);
    logline(ACLOG_DEBUG, "%s", chal);
}

bool answerchallenge(client &cl, uchar crandom[48], uchar canswer[32])
{
    if (!isdedicated){ sendf(&cl, 1, "ri2", SV_AUTH_ACR_CHAL, 2); return false; }
    if (!cl.authreq) return false;
    loopv(authrequests)
    {
        if (authrequests[i].id == cl.authreq)
        {
            sendf(&cl, 1, "ri2", SV_AUTH_ACR_CHAL, 1);
            return false;
        }
    }
    authrequest &r = authrequests.add();
    r.id = cl.authreq;
    memcpy(r.crandom, crandom, sizeof(uchar) * 48);
    memcpy(r.canswer, canswer, sizeof(uchar) * 32);
    logline(ACLOG_INFO, "[%s] answers auth #%u", cl.gethostname(), r.id);
    sendf(&cl, 1, "ri2", SV_AUTH_ACR_CHAL, 4);
    return true;
}

void masterdisc(int cn, int result)
{
    if (!valid_client(cn)) return;
    client &cl = *clients[cn];
    cl.masterdisc = result;
    if (result)
    {
        logline(ACLOG_INFO, "[%s] master disconnects client for %s%s",
            cl.gethostname(), disc_reason(result),
            cl.connectauth ? ", pending auth" : "");

        if (!cl.connectauth) checkauthdisc(cl, true);
    }
    else
    {
        logline(ACLOG_VERBOSE, "[%s] master allows client", cl.gethostname());
    }
}

void logversion(client &cl)
{
    char ctype;
    if (cl.acbuildtype & 0x40) ctype = 'W';
    else if (cl.acbuildtype & 0x20) ctype = 'M';
    else if (cl.acbuildtype & 0x04) ctype = 'L';
    else ctype = 'U';

    bool isdebug = cl.acbuildtype & 0x08;

    logline(ACLOG_INFO, "[%s] %s runs %d [%x-%c%s] [GUID-%08X]", cl.gethostname(), cl.formatname(), cl.acversion, cl.acbuildtype, ctype, isdebug ? "D" : "", cl.acguid);
}
