#include "nickserv.hpp"
#include "irc_std.hpp"
#include "config.hpp"
#include "common.hpp"


#define NICKSERV_NICK "NickServ"
#ifdef USE_NICKSERV
#include "client_state.hpp"
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <assert.h>


#define CRYPT_SALT "$6$nickserv$" // SHA512
#define PWLEN 256


static const char* nick_basename(const char* nick) { ///< strips trailing '_'s
#if 1
    return nick;
#else
    static char buf[128];
    size_t l = strlen(nick);
    while (l && nick[l-1] == '_') {
        l--;
    }
    if (l >= sizeof(buf)) {
        assert(false);
        return nick;
    }
    strncpy(buf, nick, l);
    buf[l] = '\0';
    return buf;
#endif
}


NickServ::NickServ(const char* spooldir) {
    if (!spooldir || !*spooldir) {
        dirfd = -1;
    } else {
        dirfd = open(spooldir, O_DIRECTORY|O_RDONLY);
        if (dirfd == -1) {
            LOG_ERRNO("open(%s, DIR)", spooldir);
        } else {
            LOG(NICKSERV_NICK" database at %s/", spooldir);
        }
    }
}


NickServ::~NickServ() {
    inst = NULL;
    if (dirfd != -1) {
        close(dirfd);
    }
}


bool NickServ::has_pw(const char* nick) const {
    assert(dirfd != -1);
    static struct stat ss;
    if (!ClientState::is_nick(nick)) {
        return false;
    }
    if (fstatat(dirfd, nick, &ss, 0) == -1) {
        return false;
    }
    return S_ISREG(ss.st_mode);
}


const char* NickServ::get_pw(const char* nick) const {
    assert(dirfd != -1);
    if (!ClientState::is_nick(nick)) {
        return NULL;
    }
    int fd = openat(dirfd, nick, O_RDONLY);
    if (fd == -1) {
        LOG_ERRNO("open(%s, r)", nick);
        return NULL;
    }

    struct stat ss;
    if (fstat(fd, &ss) == -1) {
        LOG_ERRNO("fstat(%s)", nick);
        close(fd);
        return NULL;
    }
    if (ss.st_size > PWLEN) {
        LOG("st_size(%s): %lld", nick, (long long)ss.st_size);
        close(fd);
        return NULL;
    }
    const size_t buflen = (size_t)ss.st_size;

    static char buf[PWLEN+1];
    ssize_t rv = read(fd, buf, buflen);
    if (rv == -1) {
        LOG_ERRNO("read(%s)", nick);
        close(fd);
        return NULL;
    } else if ((size_t)rv != buflen) {
        LOG("read(%s): %zd", nick, rv);
        close(fd);
        return NULL;
    }
    buf[buflen] = '\0';
    close(fd);
    return buf;
}


bool NickServ::set_pw(const char* nick, const char* pw) {
    assert(dirfd != -1);
    if (!ClientState::is_nick(nick)) {
        return false;
    }
    if (!pw) {
        if (unlinkat(dirfd, nick, 0) == -1) {
            LOG_ERRNO("unlink(%s)", nick);
            return false;
        }
        return true;
    }

    int fd = openat(dirfd, nick, O_WRONLY|O_CREAT|O_EXCL|O_TRUNC, S_IRUSR|S_IWUSR); // don't allow change, only re-registration
    if (fd == -1) {
        LOG_ERRNO("open(%s, w)", nick);
        return false;
    }

#ifndef NO_USE_CRYPT
    pw = crypt(pw, CRYPT_SALT); // link with -lcrypt
    if (!pw) {
        LOG_ERRNO("crypt()");
        return false;
    }
#endif

    size_t pwlen = strlen(pw);
    if (pwlen > PWLEN) {
        return false;
    }
    ssize_t rv = write(fd, pw, pwlen);
    if (rv == -1) {
        LOG_ERRNO("write(%s)", nick);
        (void)unlinkat(dirfd, nick, 0);
        close(fd);
        return false;
    } else if ((size_t)rv != pwlen) {
        LOG("write(%s): %zd", nick, rv);
        (void)unlinkat(dirfd, nick, 0);
        close(fd);
        return false;
    } else {
        close(fd);
        return true;
    }
}


bool NickServ::check_pw(const char* nick, const char* pw) const {
    const char* cpw = get_pw(nick);
    if (!cpw) {
        return false;
    }
#ifdef NO_USE_CRYPT
    return (strcmp(cpw, pw) == 0);
#else
    const char* md = crypt(pw, CRYPT_SALT);
    return (md && strcmp(cpw, md) == 0);
#endif
}


bool NickServ::reply(Client* client, const char* msg) const {
    (void)srv_reply_fmt(client, "NOTICE", NICKSERV_NICK ": %s", msg);
    return raw_reply_fmt_long(client, msg, ":%s PRIVMSG %s", NICKSERV_NICK, client->irc_state->nick);
}


bool NickServ::reg(Client* client, const char* pw) {
    if (client->irc_state->nickserv_reg) {
        return reply(client, "Your nick has already been registered.");
    }
    assert(!client->irc_state->nickserv_ident);

    if (!*pw || strcmp(pw, client->irc_state->nick) == 0 || (config.server_pw.str && strcmp(pw, config.server_pw.str) == 0)) {
        return reply(client, "Don't use this password.");
    }

    if (!set_pw(nick_basename(client->irc_state->nick), pw)) {
        return reply(client, "Error setting password.");
    }

    client->irc_state->nickserv_reg = true;
    client->irc_state->nickserv_ident = true; // and also mark as identified
    (void)reply(client, "Your nick has been registered. Reconnecting.");
    return false;
}


bool NickServ::ident(Client* client, const char* pw) {
    if (!client->irc_state->nickserv_reg) {
        return reply(client, "Your nick has not been registered.");
    }
    if (client->irc_state->nickserv_ident) {
        return reply(client, "You are already identified.");
    }

    if (!check_pw(nick_basename(client->irc_state->nick), pw)) {
        return reply(client, "Password error or mismatch.");
    }

    client->irc_state->nickserv_ident = true;
    return true; // silent if common case is ok
}


bool NickServ::drop(Client* client) {
    if (!client->irc_state->nickserv_reg) {
        return reply(client, "Your nick has not been registered.");
    }
    if (!client->irc_state->nickserv_ident) { // thus double auth?
        return reply(client, "You are not yet identified.");
    }

    if (!set_pw(nick_basename(client->irc_state->nick), NULL)) {
        return reply(client, "Error removing password.");
    }

    client->irc_state->nickserv_reg = false;
    client->irc_state->nickserv_ident = false;
    (void)reply(client, "Your nick has been unregistered. Reconnecting.");
    return false;
}


bool NickServ::is_reg(const char* nick) {
    if (dirfd == -1) { // or not enabled
        return false;
    }
    return has_pw(nick_basename(nick));
}


bool NickServ::is_enabled() {
    NickServ* i = getInst();
    return i->dirfd != -1; // enabled needed?
}


bool NickServ::handle(Client* client, const char* params[], unsigned nparams) {
    if (dirfd == -1) {
        return srv_reply_fmt(client, REPLYFMT(ERR_NOSUCHNICK), NICKSERV_NICK);
    }

    if (nparams < 1) {
        return reply(client, "Unknown " NICKSERV_NICK " command (register|identify|drop).");
    }
    const char* cmd = params[0];

    if (strcasecmp(cmd, "register") == 0) {
        if (nparams < 2) {
            return reply(client, "usage: register [NICK] PASS [EMAIL]");
        } else if (nparams == 2) {
            return reg(client, params[1]);
        } else if (strcmp(params[1], client->irc_state->nick) == 0) {
            return reg(client, params[2]);
        } else {
            return reg(client, params[1]);
        }
    } else if (strcasecmp(cmd, "identify") == 0) {
        if (nparams < 2) {
            return reply(client, "usage: identify [NICK] PASS");
        } else if (nparams == 2) {
            return ident(client, params[1]);
        } else {
            return ident(client, params[2]);
        }
    } else if (strcasecmp(cmd, "drop") == 0) { // drop [NICK] [PASS]
        return drop(client);
    } else {
        return reply(client, "Unknown " NICKSERV_NICK " command [register|identify|drop].");
    }
}


#else


NickServ::NickServ(const char* spooldir) {
    if (spooldir && *spooldir) {
        LOG(NICKSERV_NICK" configured, but support disabled!");
    }
}


NickServ::~NickServ() {
    inst = NULL;
}


bool NickServ::handle(Client* client, const char* params[], unsigned nparams) {
    return srv_reply_fmt(client, REPLYFMT(ERR_NOSUCHNICK), NICKSERV_NICK);
}

bool NickServ::is_reg(const char* nick) {
    return false;
}


bool NickServ::is_enabled() {
    return false;
}


#endif


NickServ* NickServ::inst = NULL;


NickServ* NickServ::getInst() {
    return inst ?: (inst = new NickServ(config.nickserv_dir.str));
}