#include "server.hpp"
#include "client_state.hpp"
#include "channel.hpp"
#include "nickserv.hpp"
#include "irc_std.hpp"
#include "config.hpp"
#include <assert.h>
#include <string>


Server* Server::inst = NULL;


Server::Server() {
    assert(!inst);
    inst = this;
    (void)Channel::createInsts();
}


Server::~Server() {
    Client::killall(":*** Shutting down.");
    Channel::killall();
}


void Server::disconnect(Client* client, const char* msg) {
    Channel::disconnect(client, msg ?: "closing."); // if not yet done
}


bool Server::notice(Client* client, const char* msg) {
    return srv_reply_msg(client, "NOTICE", msg, strlen(msg));
}


bool Server::motd(Client* client) const {
    return srv_reply_msg(client, REPLYMSG(RPL_WELCOME)) &&
           srv_reply_msg(client, REPLYMSG(RPL_MOTDSTART)) &&
#ifdef IRCD_VERSION
           srv_reply_msg(client, RPL_MOTD, cstrnlen(":  ircd v" STRINGIFY(IRCD_VERSION))) &&
#endif
#ifndef MESSAGE_LOG
           srv_reply_msg(client, RPL_MOTD, cstrnlen(":  No logs!")) &&
#endif
#ifdef USE_OPENSSL
           (atoi(config.ssl.str)? srv_reply_msg(client, RPL_MOTD, cstrnlen(":  SSL only!")): true) &&
#endif
           (NickServ::is_enabled()? srv_reply_msg(client, RPL_MOTD, cstrnlen(":  Please use NickServ to authenticate!")): true) &&
#if 1
           srv_reply_msg(client, RPL_MOTD, cstrnlen(":  See https://www.hackitu.de/ircd/ for more information.")) &&
#endif
           srv_reply_msg(client, REPLYMSG(RPL_ENDOFMOTD));
}


bool Server::ping(Client* client) {
    static msg_t ping;
    static size_t pinglen = 0;
    if (!pinglen) {
        pinglen = snprintf(ping, sizeof(ping), "PING :%s", config.server_name.str);
    }
    return raw_reply_msg(client, ping, pinglen);
}


bool Server::handle(Client* client, Command* cmd) {
    // no command, only tickle
    if (!cmd) {
        return ping(client);
    }

    // unknown command
    if (cmd->type->t == CMD_UNKNOWN) {
        return srv_reply_fmt(client, REPLYFMT(ERR_UNKNOWNCOMMAND), cmd->params[0]);
    } else if (cmd->nparams < cmd->type->nparams_min || cmd->nparams > cmd->type->nparams_max) {
        return srv_reply_fmt(client, REPLYFMT(ERR_NEEDMOREPARAMS), cmd->type->s);
    }

    // global server auth
    switch (cmd->type->t) {
        case CMD_PASS:
            if (config.server_pw.str && strcmp(cmd->params[0], config.server_pw.str) == 0) {
                client->irc_state->has_pass = 1;
                return true; // no reply
            } else {
                return srv_reply_msg(client, REPLYMSG(ERR_PASSWDMISMATCH));
            }

        default:
            break;
    }
    if (!client->irc_state->has_pass) {
        if (!config.server_pw.str) {
            client->irc_state->has_pass = 1;
        } else {
            return srv_reply_fmt(client, REPLYFMT(ERR_NOTREGISTERED), "server password");
        }
    }

    // (pre-)registration commands
    switch (cmd->type->t) {
        case CMD_NICK:
            if (!client->irc_state->nick) { // first time
                if (!client->irc_state->nick_set(cmd->params[0])) {
                    return srv_reply_fmt(client, REPLYFMT(ERR_NICKNAMEINUSE), cmd->params[0]);
                } else {
                    LOG("NICK %s", client->irc_state->nick);
                    return (client->irc_state->user)? motd(client): true;
                }
            }
            break; // is actually a nickchange - do this below

        case CMD_USER:
            if (client->irc_state->user) {
                return srv_reply_msg(client, REPLYMSG(ERR_ALREADYREGISTRED));
            } else if (!client->irc_state->user_set(cmd->params[0], cmd->params[3])) {
                return srv_reply_fmt(client, REPLYFMT(ERR_NOTREGISTERED), "invalid");
            } else {
                return (client->irc_state->nick)? motd(client): true;
            }

        case CMD_PING:
            return raw_reply_fmt(client,
                ":%s PONG %s :%s",
                config.server_name.str, config.server_name.str, (cmd->nparams == 1)? cmd->params[0]: config.server_name.str
            );

        case CMD_PONG: // Ignored, activity timer has been resetted by client read
            return true;

        case CMD_CAP: // TODO: extra capabilities really needed?
            if (cmd->nparams == 1 && strcmp(cmd->params[0], "LS") == 0) {
                return raw_reply_fmt(client, ":%s CAP * LS :", config.server_name.str);
            } else if (cmd->nparams == 1 && strcmp(cmd->params[0], "END") == 0) {
                return true; // ignore
            } else {
                return srv_reply_msg(client, REPLYMSG(ERR_INVALIDCAPCMD));
            }

        case CMD_QUIT:
            LOG("QUIT");
            Channel::disconnect(client, (cmd->nparams>0)? cmd->params[0]: "/quit");
            (void)raw_reply_msg(client, cstrnlen("ERROR :Closing Link: quit"));
            return false;

        default:
            break;
    }

    // post-login & -registration commands below
    if (!client->irc_state->nick || !client->irc_state->user) {
        return srv_reply_fmt(client, REPLYFMT(ERR_NOTREGISTERED), "nick/user");
    }

    // nickserv & pre-ident permissive commands
    bool b;
    switch (cmd->type->t) {
        case CMD_NICKSERV:
            b = client->irc_state->nickserv_ident;
            if (!NickServ::getInst()->handle(client, (const char**)cmd->params, cmd->nparams)) {
                return false;
            }
            if (!b && client->irc_state->nickserv_ident) {
                LOG("NICKSERV ident");
                if (!Channel::mode(client)) {
                    return false;
                }
            }
            return true;

        case CMD_OPER:
            if (NickServ::is_enabled() && !client->irc_state->nickserv_reg) { // cannot get ident lateron
                return srv_reply_fmt(client, REPLYFMT(ERR_NOTREGISTERED), "nickserv unregistered");
            }
            if (strcmp(client->irc_state->nick, cmd->params[0]) != 0) { // let grant only for himself
                return srv_reply_msg(client, REPLYMSG(ERR_PASSWDMISMATCH));
            }
            if (!config.oper_pw.str || strcmp(config.oper_pw.str, cmd->params[1]) != 0) {
                return srv_reply_msg(client, REPLYMSG(ERR_PASSWDMISMATCH));
            }
            if (!client->irc_state->is_oper) {
                client->irc_state->is_oper = true; // might be hidden if not identified, though
                LOG("OPER");
                if (!Channel::mode(client)) {
                    return false;
                }
            }
            return srv_reply_msg(client, REPLYMSG(RPL_YOUREOPER));

        case CMD_LIST:
            return Channel::list(client);

        case CMD_AWAY:
            (void)client->irc_state->away_set((cmd->nparams == 0)? NULL: cmd->params[0]); // no error reply?
            if (client->irc_state->away) {
                return srv_reply_msg(client, REPLYMSG(RPL_NOWAWAY));
            } else {
                return srv_reply_msg(client, REPLYMSG(RPL_UNAWAY));
            }

        case CMD_MODE:
            if (strcmp(cmd->params[0], client->irc_state->nick) == 0) {
                // no (own) usermodes except +i (don't globally allow WHO and NAMES anyhow)
                return (cmd->nparams == 2 && !strcmp(cmd->params[1], "+i"))? true:
                       srv_reply_msg(client, REPLYMSG(ERR_UMODEUNKNOWNFLAG));
            }
            break; // channel modes below

        case CMD_MOTD:
            return motd(client);

        default:
            break;
    }

    // no unident below this point
    if (client->irc_state->nickserv_reg && !client->irc_state->nickserv_ident) {
        return srv_reply_fmt(client, REPLYFMT(ERR_NOTREGISTERED), "nickserv protected nick");
    }
    if (client->irc_state->is_oper && NickServ::is_enabled() && !client->irc_state->nickserv_ident) { // oper only for registered users, not checked below as ordering might not be given
        return srv_reply_fmt(client, REPLYFMT(ERR_NOTREGISTERED), "nickserv protected oper");
    }

    // normal operation commands
    char* s;
    std::string ss;
    Channel* channel;
    switch (cmd->type->t) {
        case CMD_NICK:
            assert(client->irc_state->nick); // already registered
            ss.assign(client->irc_state->nick);
            if (!client->irc_state->nick_set(cmd->params[0])) {
                return srv_reply_fmt(client, REPLYFMT(ERR_NICKNAMEINUSE), cmd->params[0]);
            } else {
                LOG("NICK %s -> %s", ss.c_str(), client->irc_state->nick);
                return Channel::nickchange(ss.c_str(), client); // announce nick change in channels
            }

        case CMD_JOIN: // TODO: support ,-separated list also at part, privmsg; there, support "0" for part'ing all
            s = strtok(cmd->params[0], ",");
            if (!s) {
                return srv_reply_fmt(client, REPLYFMT(ERR_NEEDMOREPARAMS), cmd->type->s);
            }
            while (s) {
                channel = Channel::getInst(s, !client->irc_state->is_oper || atoi(config.oper_channels.str) != 1);
                if (!channel) {
                    if (!srv_reply_fmt(client, REPLYFMT(ERR_NOSUCHCHANNEL), cmd->params[0])) return false;
                } else {
                    if (!channel->join(client)) {
                        channel->cleanup();
                        return false; // channel replies itself
                    } else {
                        LOG("JOIN %s", channel->name);
                    }
                    channel->cleanup(); // in case joining failed
                }
                s = strtok(NULL, ",");
            }
            return true;

        case CMD_PART:
            channel = Channel::getInst(cmd->params[0], true);
            if (!channel) {
                return srv_reply_fmt(client, REPLYFMT(ERR_NOSUCHCHANNEL), cmd->params[0]);
            }
            LOG("PART %s", channel->name);
            return channel->part(client, (cmd->nparams>1)? cmd->params[1]: NULL);

        case CMD_WHO:
            channel = Channel::getInst(cmd->params[0], true);
            if (!channel) {
                return srv_reply_fmt(client, REPLYFMT(ERR_NOSUCHCHANNEL), cmd->params[0]);
            }
            return channel->who(client);

        case CMD_MODE:
            channel = Channel::getInst(cmd->params[0], true);
            if (!channel) { // own user modes above
                return srv_reply_fmt(client, REPLYFMT(ERR_NOSUCHCHANNEL), cmd->params[0]);
            }
            return channel->mode_get(client);

        case CMD_TOPIC:
            channel = Channel::getInst(cmd->params[0], true);
            if (!channel) {
                return srv_reply_fmt(client, REPLYFMT(ERR_NOSUCHCHANNEL), cmd->params[0]);
            }
            if (cmd->nparams == 1) {
                return channel->topic(client);
            } else {
                return channel->topic(client, cmd->params[1]);
            }

        case CMD_PRIVMSG:
            channel = Channel::getInst(cmd->params[0], true);
            if (channel) {
                return channel->privmsg(client, cmd->params[1]);
            } else {
                Client* c = ClientState::nick_find(cmd->params[0]);
                if (c && Channel::is_peer(client, c)) { // don't allow non-peers, could be e.g. unidentified - how's the nick's presence known anyways?
                    (void)raw_reply_fmt_long(c, cmd->params[1], ":%s PRIVMSG %s",
                        client->irc_state->prefix_get(true, client == c || c->irc_state->is_oper || strncmp("\x01""DCC ", cmd->params[1], 5) == 0),
                        cmd->params[0]
                    );
                    if (c->irc_state->away) {
                        return srv_reply_fmt(client, REPLYFMT(RPL_AWAY), c->irc_state->nick, c->irc_state->away);
                    }
                    return true;
                } else {
                    return srv_reply_fmt(client, REPLYFMT(ERR_NOSUCHNICK), cmd->params[0]);
                }
            }

        default:
            break;
    }

    assert(false);
    return false;
}