// Cyphesis Online RPG Server and AI Engine
// Copyright (C) 2001 Alistair Riddoch
//
// 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; either version 2 of the License, or
// (at your option) any later version.
// 
// 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

// $Id: 60509741c54d67b7cca4d9921b385e106d82c6fd $

/// \page cydb_index
///
/// \section Introduction
///
/// cydb is a commandline tool to administrate the server database. For
/// information on the commands available, please see the unix manual page.
/// The manual page is generated from docbook sources, so can
/// also be converted into other formats.

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "common/log.h"
#include "common/globals.h"
#include "common/system.h"
#include "common/Storage.h"

#include <varconf/config.h>

#include "common/compose.hpp"

#ifdef HAVE_GETOPT_H
#include <getopt.h>
#endif // HAVE_GETOPT_H

#ifndef READLINE_CXX_SANE   // defined in config.h
extern "C" {
#endif
#define USE_VARARGS
#define PREFER_STDARG
#include <readline/readline.h>
#include <readline/history.h>
#ifndef READLINE_CXX_SANE
}
#endif

typedef int (*dbcmd_function)(Storage & ab, struct dbsys * system,
                              int argc, char ** argv);

/// \brief Entry in the global command table for cycmd
struct dbsys {
    const char * sys_name;
    const char * sys_description;
    dbcmd_function sys_function;
    struct dbsys * sys_subsys;
};

int dbs_help(Storage & ab, struct dbsys * system, int argc, char ** argv)
{
    size_t max_length = 0;

    for (struct dbsys * I = system->sys_subsys; I->sys_name != NULL; ++I) {
       max_length = std::max(max_length, strlen(I->sys_name));
    }
    max_length += 2;

    std::cout << "Cyphesis database tool." << std::endl
              << system->sys_name << " commands:"
              << std::endl << std::endl;

    for (struct dbsys * I = system->sys_subsys; I->sys_name != NULL; ++I) {
        std::cout << "    " << I->sys_name
                  << std::string(max_length - strlen(I->sys_name), ' ')
                  << I->sys_description << std::endl;
    }
    std::cout << std::endl << std::flush;
    return 0;
}


static int world_purge(Storage & ab, struct dbsys * system,
                      int argc, char ** argv)
{
    std::string cmd = "DELETE FROM entities WHERE loc IS NOT null";
    if (Database::instance()->runCommandQuery(cmd) != 0) {
        std::cout << "Entity purge fail" << std::endl << std::flush;
        return 1;
    }
    cmd = "DELETE FROM properties";
    if (Database::instance()->runCommandQuery(cmd) != 0) {
        std::cout << "Property purge fail" << std::endl << std::flush;
        return 1;
    }
    return 0;
}

static int users_purge(Storage & ab, struct dbsys * system,
                      int argc, char ** argv)
{
    std::string cmd = "DELETE FROM accounts WHERE username != 'admin'";
    if (Database::instance()->runCommandQuery(cmd) != 0) {
        std::cout << "User purge fail" << std::endl << std::flush;
        return 1;
    }
    return 0;
}

static int users_list(Storage & ab, struct dbsys * system,
                      int argc, char ** argv)
{
    std::string cmd = "SELECT username, type FROM accounts";
    DatabaseResult res = Database::instance()->runSimpleSelectQuery(cmd);
    DatabaseResult::const_iterator I = res.begin();
    DatabaseResult::const_iterator Iend = res.end();
    for (; I != Iend; ++I) {
        std::string name = I.column("username");
        std::string type = I.column("type");
        std::cout << (type == "admin" ? "*" : " ") << name
                  << std::endl << std::flush;
    }

    return 0;
}

static int users_del(Storage & ab, struct dbsys * system,
                     int argc, char ** argv)
{
    if (argc != 2) {
        std::cout << "usage: " << system->sys_name
                  << " <username>" << std::endl << std::flush;
        return 1;
    }
    std::string id = argv[1];
    std::string cmd = String::compose("SELECT username FROM accounts "
                                      "WHERE username='%1'", id);
    DatabaseResult res = Database::instance()->runSimpleSelectQuery(cmd);
    if (res.size() == 0) {
        std::cout << "User account " << id << " not found"
                  << std::endl << std::flush;
        return 1;
    }
    if (res.size() != 1) {
        std::cout << "ERROR: Multiple accounts match " << id
                  << std::endl << std::flush;
        return 1;
    }
    cmd = String::compose("DELETE FROM accounts WHERE "
                          "username = '%1'", id);
    if (Database::instance()->runCommandQuery(cmd) != 0) {
        std::cout << "User delete fail" << std::endl << std::flush;
        return 1;
    }

    return 0;
}

static int users_mod(Storage & ab, struct dbsys * system,
                     int argc, char ** argv)
{
    int opt;
    char * type = 0, * password = 0;

    while ((opt = getopt(argc, argv, "t:p:")) != -1) {
        switch (opt) {
          case 't':
            type = optarg;
            break;
          case 'p':
            password = optarg;
            break;
          default:
            std::cout << "usage: " << system->sys_name
                      << " [-t TYPE|-p PASSWORD] USERNAME"
                      << std::endl << std::flush;
            return 1;
        }
    }

    if (argc - optind != 1 || (type == 0 && password == 0)) {
        std::cout << "usage: " << system->sys_name
                  << " [-t TYPE] [-p PASSWORD] USERNAME"
                  << std::endl << std::flush;
        return 1;
    }
    std::string id = argv[optind];
    std::string cmd = String::compose("SELECT username, type FROM accounts "
                                      "WHERE username='%1'", id);
    DatabaseResult res = Database::instance()->runSimpleSelectQuery(cmd);
    if (res.size() == 0) {
        std::cout << "User account " << id << " not found"
                  << std::endl << std::flush;
        return 1;
    }
    if (res.size() != 1) {
        std::cout << "ERROR: Multiple accounts match " << id
                  << std::endl << std::flush;
        return 1;
    }
    if (type != 0) {
        std::string new_type = type;
        if (new_type != "admin" && new_type != "player" &&
            new_type != "disabled") {
            std::cout << "ERROR: Account type must be one of "
                         "\"player\", \"admin\" or \"disabled\""
                      << std::endl << std::flush;
        }
        // FIXME Verify the account exists.
        cmd = String::compose("UPDATE accounts SET type = '%1' WHERE "
                              "username = '%2'", new_type, id);
        if (Database::instance()->runCommandQuery(cmd) != 0) {
            std::cout << "User mod type fail" << std::endl << std::flush;
            return 1;
        }
        std::cout << "Account type updated."
                  << std::endl << std::flush;
    }
    if (password != 0) {
        std::string new_pass;
        encrypt_password(password, new_pass);
        cmd = String::compose("UPDATE accounts SET password = '%1' WHERE "
                              "username = '%2'", new_pass, id);
        if (Database::instance()->runCommandQuery(cmd) != 0) {
            std::cout << "User mod password fail" << std::endl << std::flush;
            return 1;
        }
        std::cout << "Account password updated."
                  << std::endl << std::flush;
    }

    return 0;
}

static int rules_purge(Storage & ab, struct dbsys * system,
                      int argc, char ** argv)
{
    std::string cmd = "DELETE FROM rules";
    if (Database::instance()->runCommandQuery(cmd) != 0) {
        std::cout << "Rule purge fail" << std::endl << std::flush;
        return 1;
    }
    return 0;
}

static int rules_list(Storage & ab, struct dbsys * system,
                      int argc, char ** argv)
{
    std::string cmd = "SELECT id FROM rules";
    DatabaseResult res = Database::instance()->runSimpleSelectQuery(cmd);
    DatabaseResult::const_iterator I = res.begin();
    DatabaseResult::const_iterator Iend = res.end();
    for (; I != Iend; ++I) {
        std::string name = I.column("id");
        std::cout << name << std::endl << std::flush;
    }

    return 0;
}

int dbs_generic(Storage & ab, struct dbsys * system,
                int argc, char ** argv)
{
    struct dbsys * subsyss = system->sys_subsys;
    if (subsyss == 0) {
        std::cout << "INTERNAL ERROR" << std::endl << std::flush;
        return 1;
    }
    int nargc = argc - 1;
    if (nargc < 1) {
        dbs_help(ab, system, argc, argv);
        return 1;
    }
    char ** nargv = &argv[1];
    for (struct dbsys * I = subsyss; I->sys_name != NULL; ++I) {
        if (strcmp(nargv[0], I->sys_name) == 0) {
            return I->sys_function(ab, I, nargc, nargv);
        }
    }
    std::cout << "ERROR: No such command: "
              << system->sys_name << " " << nargv[0]
              << std::endl << std::endl << std::flush;
    dbs_help(ab, system, argc, argv);
    return 0;
}

struct dbsys world_cmds[] = {
    { "purge", "Purge world data", &world_purge, 0 },
    { "help",  "Show world help", &dbs_help, &world_cmds[0] },
    { NULL,    "Guard", }
};

struct dbsys users_cmds[] = {
    { "purge", "Purge users data", &users_purge, 0 },
    { "list",  "List user accounts", &users_list, 0 },
    { "del",   "Delete a user account", &users_del, 0 },
    { "mod",   "Modify a user account", &users_mod, 0 },
    { "help",  "Show users help", &dbs_help, &users_cmds[0] },
    { NULL,    "Guard", }
};

struct dbsys rules_cmds[] = {
    { "purge", "Purge rules data", &rules_purge, 0 },
    { "list",  "List rules", &rules_list, 0 },
    { "help",  "Show rules help", &dbs_help, &rules_cmds[0] },
    { NULL,    "Guard", }
};

struct dbsys systems[] = {
    { "rules", "Modify the rule storage table", &dbs_generic, &rules_cmds[0] },
    { "user",  "Modify the user account table", &dbs_generic, &users_cmds[0] },
    { "world", "Modify the world storage tables", &dbs_generic, &world_cmds[0] },
    { "help",  "Show command help", &dbs_help, &systems[0] },
    { NULL,    "Guard", }
};

struct dbsys tool = {
    "cydb",  "Work with cyphesis databases", &dbs_help, &systems[0]
};

int completion_iterator = 0;

char * completion_generator(const char * text, int state)
{
    if (state == 0) {
        completion_iterator = 0;
    }
    for (int i = completion_iterator; systems[i].sys_name != 0; ++i) {
        if (strncmp(text, systems[i].sys_name, strlen(text)) == 0) {
            completion_iterator = i + 1;
            return strdup(systems[i].sys_name);
        }
    }
    return 0;
}

void gotCommand(char * cmd)
{
}

// FIXME
void setup_completion()
{
    std::string prompt="foobar>";
    rl_callback_handler_install(prompt.c_str(), &gotCommand);
    rl_completion_entry_function = &completion_generator;

    std::cout << std::endl << std::flush;
    rl_callback_handler_remove();
}

#if 0
// FIXME
void updatePrompt()
{
    std::string prompt;
    std::string designation(">");
    if (!username.empty()) {
        prompt = username + "@";
        if (accountType == "admin") {
            designation = "#";
        } else {
            designation = "$";
        }
    } else {
        prompt = "";
    }
    prompt += serverName;
    prompt += " ";
    prompt += systemType;
    prompt += designation;
    prompt += " ";
    rl_set_prompt(prompt.c_str());
}

void exec(const std::string & cmd, const std::string & arg)
{
    bool reply_expected = true;
    reply_flag = false;
    error_flag = false;

    if (cmd == "stat") {
        Get g;
        encoder->streamObjectsMessage(g);
    } else if (cmd == "install") {
        size_t space = arg.find(' ');
        if (space == std::string::npos || space >= (arg.size() - 1)) {
            std::cout << "usage: install <type id> <parent id>"
                      << std::endl << std::flush;
        } else {
            Create c;
            c->setFrom(accountId);
            Anonymous ent;
            ent->setId(std::string(arg, 0, space));
            ent->setObjtype("class");
            ent->setParents(std::list<std::string>(1, std::string(arg, space + 1)));
            c->setArgs1(ent);
            encoder->streamObjectsMessage(c);
        }
        reply_expected = false;
    } else if (cmd == "look") {
        Look l;
        l->setFrom(accountId);
        encoder->streamObjectsMessage(l);
    } else if (cmd == "logout") {
        Logout l;
        l->setFrom(accountId);
        if (!arg.empty()) {
            Anonymous lmap;
            lmap->setId(arg);
            l->setArgs1(lmap);
            reply_expected = false;
        }
        encoder->streamObjectsMessage(l);
    } else if (cmd == "say") {
        Talk t;
        Anonymous ent;
        ent->setAttr("say", arg);
        t->setArgs1(ent);
        t->setFrom(accountId);
        encoder->streamObjectsMessage(t);
    } else if (cmd == "help" || cmd == "?") {
        reply_expected = false;
        help();
    } else if (cmd == "query") {
        Get g;

        Anonymous cmap;
        cmap->setObjtype("obj");
        if (!arg.empty()) {
            cmap->setId(arg);
        }
        g->setArgs1(cmap);
        g->setFrom(accountId);

        encoder->streamObjectsMessage(g);
    } else if (cmd == "reload") {
        if (arg.empty()) {
            reply_expected = false;
            std::cout << "reload: Argument required" << std::endl << std::flush;
        } else {
            Set s;

            Anonymous tmap;
            tmap->setObjtype("class");
            tmap->setId(arg);
            s->setArgs1(tmap);
            s->setFrom(accountId);

            encoder->streamObjectsMessage(s);
        }
    } else if (cmd == "get") {
        Get g;

        Anonymous cmap;
        cmap->setObjtype("class");
        if (!arg.empty()) {
            cmap->setId(arg);
        }
        g->setArgs1(cmap);
        g->setFrom(accountId);

        encoder->streamObjectsMessage(g);
    } else if (cmd == "monitor") {
        AdminTask * task = new OperationMonitor;
        if (runTask(task, arg) == 0) {
            Monitor m;

            m->setArgs1(Anonymous());
            m->setFrom(accountId);

            encoder->streamObjectsMessage(m);
        }

        reply_expected = false;
    } else if (cmd == "unmonitor") {
        OperationMonitor * om = dynamic_cast<OperationMonitor *>(currentTask);

        if (om != 0) {
            Monitor m;

            m->setFrom(accountId);

            encoder->streamObjectsMessage(m);

            reply_expected = false;

            struct timeval tv;
            gettimeofday(&tv, NULL);
            int monitor_time = tv.tv_sec - om->startTime();

            std::cout << om->count() << " operations monitored in "
                      << monitor_time << " seconds = "
                      << om->count() / monitor_time
                      << " operations per second"
                      << std::endl << std::flush;

            endTask();
        }
    } else if (cmd == "connect") {
        reply_expected = false;
        Connect m;

        Anonymous cmap;
        cmap->setAttr("hostname", arg);
        m->setArgs1(cmap);
        m->setFrom(accountId);

        encoder->streamObjectsMessage(m);
    } else if (cmd == "add_agent") {
        std::string agent_type("creator");

        if (!arg.empty()) {
            agent_type = arg;
        }
        
        Create c;

        Anonymous cmap;
        cmap->setParents(std::list<std::string>(1, agent_type));
        cmap->setName("cycmd agent");
        cmap->setObjtype("obj");
        c->setArgs1(cmap);
        c->setFrom(accountId);

        avatar_flag = true;

        encoder->streamObjectsMessage(c);
    } else if (cmd == "delete") {
        if (agentId.empty()) {
            std::cout << "Use add_agent to add an in-game agent first" << std::endl << std::flush;
            reply_expected = false;
        } else if (arg.empty()) {
            std::cout << "Please specify the entity to delete" << std::endl << std::flush;
            reply_expected = false;
        } else {
            Delete del;

            Anonymous del_arg;
            del_arg->setId(arg);
            del->setArgs1(del_arg);
            del->setFrom(agentId);
            del->setTo(arg);

            encoder->streamObjectsMessage(del);

            reply_expected = false;
        }
    } else if (cmd == "find_by_name") {
        if (agentId.empty()) {
            std::cout << "Use add_agent to add an in-game agent first" << std::endl << std::flush;
            reply_expected = false;
        } else if (arg.empty()) {
            std::cout << "Please specify the name to search for" << std::endl << std::flush;
            reply_expected = false;
        } else {
            Look l;

            Anonymous lmap;
            lmap->setName(arg);
            l->setArgs1(lmap);
            l->setFrom(agentId);

            encoder->streamObjectsMessage(l);

            reply_expected = false;
        }
    } else if (cmd == "find_by_type") {
        if (agentId.empty()) {
            std::cout << "Use add_agent to add an in-game agent first" << std::endl << std::flush;
            reply_expected = false;
        } else if (arg.empty()) {
            std::cout << "Please specify the type to search for" << std::endl << std::flush;
            reply_expected = false;
        } else {
            Look l;

            Anonymous lmap;
            lmap->setParents(std::list<std::string>(1, arg));
            l->setArgs1(lmap);
            l->setFrom(agentId);

            encoder->streamObjectsMessage(l);

            reply_expected = false;
        }
    } else if (cmd == "flush") {
        if (agentId.empty()) {
            std::cout << "Use add_agent to add an in-game agent first" << std::endl << std::flush;
            reply_expected = false;
        } else if (arg.empty()) {
            std::cout << "Please specify the type to flush" << std::endl << std::flush;
            reply_expected = false;
        } else {
            AdminTask * task = new Flusher(agentId);
            runTask(task, arg);
            reply_expected = false;
        }
    } else if (cmd == "creator_create") {
        if (agentId.empty()) {
            std::cout << "Use add_agent to add an in-game agent first" << std::endl << std::flush;
            reply_expected = false;
        } else if (arg.empty()) {
            std::cout << "Use add_agent to add an in-game agent first" << std::endl << std::flush;
            reply_expected = false;
        } else {
            Create c;

            Anonymous thing;
            thing->setParents(std::list<std::string>(1, arg));
            c->setArgs1(thing);
            c->setFrom(agentId);

            encoder->streamObjectsMessage(c);

            reply_expected = false;
        }
    } else if (cmd == "cancel") {
        if (endTask() != 0) {
            std::cout << "No task currently running" << std::endl << std::flush;
        }
    } else {
        reply_expected = false;
        std::cout << cmd << ": Command not known" << std::endl << std::flush;
    }

    ios << std::flush;

    if (!reply_expected) { return; }
    // Wait for reply
    time_t wait_start_time = time(NULL);
    while (!reply_flag) {
       if (time(NULL) - wait_start_time > 5) {
           std::cout << cmd << ": No reply from server" << std::endl << std::flush;
           return;
       }
       poll(false);
    }
}
#endif

static int run_command(Storage & ab, int argc, char ** argv)
{
    for (struct dbsys * I = &systems[0]; I->sys_name != NULL; ++I) {
        if (strcmp(argv[0], I->sys_name) == 0) {
            return I->sys_function(ab, I, argc, argv);
        }
    }
    std::cout << "ERROR: No such command: " << argv[0]
              << std::endl << std::endl << std::flush;
    dbs_help(ab, &tool, argc, argv);
    return 1;
}

int main(int argc, char ** argv)
{
    int config_status = loadConfig(argc, argv, USAGE_DBASE); 
    if (config_status < 0) {
        if (config_status == CONFIG_VERSION) {
            reportVersion(argv[0]);
            return 0;
        } else if (config_status == CONFIG_HELP) {
            showUsage(argv[0], USAGE_DBASE, "[ cmd ]");
            return 0;
        } else if (config_status != CONFIG_ERROR) {
            log(ERROR, "Unknown error reading configuration.");
        }
        // Fatal error loading config file
        return 1;
    }

    int optind = config_status;

    bool interactive = true;
    int ret = 0;
    std::string cmd;
    if (optind < argc) {
        cmd = argv[optind];
        interactive = false;
    }

    Storage ab;

    if (ab.init() != 0) {
        log(ERROR, "Database setup failed.");
        return 1;
    }

    if (!interactive) {
        ret = run_command(ab, argc - optind, &argv[optind]);
    } else {
        dbs_help(ab, &tool, argc, argv);
    }

    delete global_conf;
    return ret;
}
