/* eurephiadm.c  --  CLI based admin program for eurephia
 *
 *  GPLv2 only - Copyright (C) 2008 - 2012
 *               David Sommerseth <dazo@users.sourceforge.net>
 *
 *  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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
*/

/**
 * @file   eurephiadm.c
 * @author David Sommerseth <dazo@users.sourceforge.net>
 * @date   2008-11-28
 *
 * @brief  Main part of the eurephiadm command line administration utility for eurephia
 *
 */


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <libgen.h>

#include <libxml/tree.h>

#include <eurephia_nullsafe.h>
#include <eurephia_context.h>
#include <eurephia_log.h>
#include <eurephia_values.h>
#include <eurephia_xml.h>
#include <eurephiadb_driver.h>
#include <eurephiadb_session_common.h>
#include <eurephiadb.h>

#define MODULE "eurephiadm" /**< Need to define the active module before including argparser.h */
#include "argparser.h"
#include "client_context.h"
#include "client_config.h"
#include "client_session.h"
#include "get_console_input.h"
#include "commands.h"


/**
 * Prints version and copyright info to stdout
 *
 * @param fprg Full path to binary
 *
 * @return Returns the basename part of the binary file name
 */
char *print_version(char *fprg) {
        char *prg = basename(fprg);

        fprintf(stdout, "%s (v%s) - eurephia administration utility\n"
                "Copyright (C) 2008-2012  David Sommerseth <dazo@users.sourceforge.net>\n",
                prg, EUREPHIAVERSION);
        return prg;
}


/**
 * Wrapper function to be used by the command API in eurephiadm
 *
 */
void help_Help() {
        char *argv[] = { MODULE, NULL };

        cmd_Help(NULL, NULL, NULL, 1, argv);
}


/**
 * Displays a generic help screen, if no arguments where given, or else the
 * the help screen for the eurephiadm command if it was found.
 *
 * @param ctx   eurephiaCTX
 * @param sess  eurephiaSESSION of the current logged in user
 * @param cfg   eurephiaVALUES struct of the current configuration
 * @param argc  argument count for the eurephiadm command
 * @param argv  argument table for the eurephiadm command
 *
 * @return returns 0 on success, otherwise 1.
 */
int cmd_Help(eurephiaCTX *ctx, eurephiaSESSION *sess, eurephiaVALUES *cfg, int argc, char **argv) {
        eurephiadm_functions *func = NULL;
        int i;
        char *prg = NULL;

        // Print the modules help screen if module name is given
        if( ((argc == 2) && (strcmp(argv[0], "help") == 0))
            || ((argc == 3) && (*argv[1] == '-')) ) {
                char *help = (argc == 2 ? argv[1] : argv[2]);

                for( i = 0; cmdline_functions[i].command != NULL; i++ ) {
                        func = (eurephiadm_functions *)&cmdline_functions[i];
                        if( (strcmp(func->command, help) == 0) && (func->helpfunc != NULL) ) {
                                func->helpfunc();
                                return 0;
                        }
                }
                fprintf(stderr, "%s: ERROR :: Could not find help for module '%s'\n", MODULE , help);
                return 1;
        }

        prg = print_version((strcmp(argv[0], "help") == 0) ? MODULE : argv[0]);

        fprintf(stdout, "\n    Usage: %s [global options] <command> [command options]\n\n", prg);

        fprintf(stdout,"        %-20.20s %-20.20s %-30.30s\n",
                "-V | --version", "", "Print version of eurephiadm");
        fprintf(stdout,"        %-20.20s %-20.20s %-30.30s\n",
                "-l | --log", "<file name>", "Log to file");
        fprintf(stdout,"        %-20.20s %-20.20s %-30.30s\n",
                "-L | --log-level", "<log level>", "Set log verbosity");


        fprintf(stdout,"\n        %-20.20s %-20.20s %-30.30s\n",
                "Command","Arguments","Description");
        fprintf(stdout, "        ------------------------------------------------------------------------\n");

        for( i = 0; cmdline_functions[i].command != NULL; i++ ) {
                func = (eurephiadm_functions *)&cmdline_functions[i];
                fprintf(stdout,"        %-20.20s %-20.20s %-30.30s\n",
                        func->command,
                        (func->arghint != NULL ? func->arghint : ""),
                        func->helpinfo);
        }
        fprintf(stdout, "\n");
        return 0;
}


/**
 * eurephiadm logout command.  Logs out the current active session.
 *
 * @param ctx   eurephiaCTX
 * @param sess  eurephiaSESSION of the current logged in user
 * @param cfg   eurephiaVALUES struct of the current configuration
 * @param argc  argument count for the logout command
 * @param argv  argument table for the logout command
 *
 * @return returns 0 on success, otherwise 1.
 */
int cmd_Logout(eurephiaCTX *ctx, eurephiaSESSION *sess, eurephiaVALUES *cfg, int argc, char **argv) {
        xmlDoc *logoutxml = NULL, *resxml = NULL;
        xmlNode *logout_n = NULL;
        eurephiaRESULT *res = NULL;
        int rc = 0;

        eurephiaXML_CreateDoc(ctx, 1, "Register", &logoutxml, &logout_n);
        assert( (logoutxml != NULL) && (logout_n != NULL) );
        xmlNewProp(logout_n, (xmlChar *) "mode", (xmlChar *) "logout");
        xmlAddChild(logout_n, xmlNewText((xmlChar *) argv[0])); // A dirty hack, but the sesskey is here

        resxml = eDBadminAuthenticate(ctx, logoutxml);
        xmlFreeDoc(logoutxml);
        if( !eurephiaXML_IsResultMsg(ctx, resxml) ) {
                eurephia_log(ctx, LOG_FATAL, 0, "Failed to logout session");
                rc = 1;
        } else {
                res = eurephiaXML_ParseResultMsg(ctx, resxml);
                if( res->resultType == exmlERROR ) {
                        fprintf(stderr, "%s: %s\n", MODULE, res->message);
                        rc = 1;
                } else {
                        fprintf(stdout, "%s: %s\n", MODULE, res->message);
                        rc = 0;
                }
        }
        xmlFreeDoc(resxml);
        free_nullsafe(ctx, res);
        return rc;
}


/**
 * eurephiadm show-config command.  Dumps configuration to stdout.  If the user is logged in, the
 * configuration saved in the database is also dumped.
 *
 * @param ctx   eurephiaCTX
 * @param sess  eurephiaSESSION of the current logged in user
 * @param cfg   eurephiaVALUES struct of the current configuration
 * @param argc  argument count for the show-config command
 * @param argv  argument table for the show-config command
 *
 * @return returns 0 on success, otherwise 1.
 */
int cmd_ShowCfg(eurephiaCTX *ctx, eurephiaSESSION *sess, eurephiaVALUES *cfg, int argc, char **argv) {
        void dump_values(eurephiaVALUES *vls) {
                eurephiaVALUES *ptr = NULL;
                fprintf(stdout, "----------------------------------------------------------------------\n");
                for( ptr = vls; ptr != NULL; ptr = ptr->next) {
                        if( ptr->key != NULL ) {
                                fprintf(stdout, "%20s = %s\n", ptr->key, ptr->val);
                        }
                }
                fprintf(stdout, "----------------------------------------------------------------------\n");
        }

        if( cfg != NULL ) {
                fprintf(stdout, "\n** eurephiadm configuration settings (config file) **\n");
                dump_values(cfg);
                fprintf(stdout, "\n");
        }
        if( ctx != NULL ) {
                fprintf(stdout, "** eurephia configuration settings (set in database) **\n");
                dump_values(ctx->dbc->config);
                fprintf(stdout, "\n");
        }
        return 0;
}


/**
 * Establish a connection to the database and register the database connection in the eurephiaCTX
 *
 * @param ctx    eurephiaCTX
 * @param argstr argument string for the database connection
 *
 * @return Returns 1 on success, otherwise 0.
 */
int eurephia_ConnectDB(eurephiaCTX *ctx, const char *argstr) {
        char *delims = " ";
        char *cp = NULL;
        const char *dbargv[MAX_ARGUMENTS];
        int dbargc = 0;

        if( (argstr == NULL) || (strlen(argstr) < 1) ) {
                eurephia_log(ctx, LOG_FATAL, 0, "No database connection string given");
                return 0;
        }

        // Split up argstr into separate arguments
        cp = strdup(argstr);
        assert(cp != NULL);
        
        dbargc = 0;
        dbargv[dbargc] = strtok(cp, delims);
        while( (dbargc <= MAX_ARGUMENTS) && (dbargv[dbargc] != NULL) ) {
                dbargv[++dbargc] = strtok(NULL, delims);
        }

        if( !eDBconnect(ctx, dbargc, dbargv) ) {
                eurephia_log(ctx, LOG_PANIC, 0, "Could not connect to the database");
                eDBlink_close(ctx);
                return 0;
        }
        free_nullsafe(ctx, cp);
       
        return 1;
}


/**
 * Log in a user.  The function will ask for user name and password and authenticate the user against
 * the eurephia database and make sure the user is granted access to the requested access level.
 *
 * @param ctx         eurephiaCTX
 * @param cfg         eurephiaVALUES with configuration settings
 * @param req_access  String (const char *) containing the requested access level.
 *
 * @return Returns a eurephiaSESSION pointer to the authenticated user session on success, otherwise NULL.
 */
static eurephiaSESSION *do_login(eurephiaCTX *ctx, eurephiaVALUES *cfg, const char *req_access) {
        xmlDoc *loginxml = NULL, *resxml = NULL;
        xmlNode *login_n = NULL;
        eurephiaRESULT *res = NULL;
        eurephiaSESSION *session = NULL;
        char username[33], password[33], *tmp = NULL;

        memset(&username, 0, 33);
        memset(&password, 0, 33);
        if( (tmp = eGet_value(cfg, "username")) == NULL ) {
                memset(username, 0, 33);
                get_console_input(username, 32, "User:", 0);
        } else {
                strncpy(username, tmp, 32);
        }
        get_console_input(password, 32, "Password:", 1);

        eurephiaXML_CreateDoc(ctx, 1, "Authenticate", &loginxml, &login_n);
        assert( (loginxml != NULL) && (login_n != NULL) );
        xmlNewProp(login_n, (xmlChar *) "mode", (xmlChar *) "user");

        xmlNewChild(login_n, NULL, (xmlChar *) "username", (xmlChar *) username);
        xmlNewChild(login_n, NULL, (xmlChar *) "password", (xmlChar *) password);
        xmlNewChild(login_n, NULL, (xmlChar *) "accesslevel", (xmlChar *) req_access);
        resxml = eDBadminAuthenticate(ctx, loginxml);
        xmlFreeDoc(loginxml);
        memset(username, 0, 33);
        memset(password, 0, 33);
        if( !eurephiaXML_IsResultMsg(ctx, resxml)  ) {
                return NULL;
        }

        res = eurephiaXML_ParseResultMsg(ctx, resxml);
        if( res->resultType == exmlERROR ) {
                fprintf(stderr, "%s: %s\n", MODULE, res->message);
                free_nullsafe(ctx, res);
                xmlFreeDoc(resxml);
                return NULL;
        }

        session = create_session(ctx, NULL);
        login_n = xmlFindNode(res->details, "UserAccount");

        // Borrow username for converting uid to a string
        snprintf(username, 30, "%s", xmlGetAttrValue(login_n->properties, "uid"));
        xmlFreeDoc(resxml);
        free_nullsafe(ctx, res);

        if( !eDBset_session_value(ctx, session, "uid", username) ) {
                eurephia_log(ctx, LOG_FATAL, 0, "Could not update session variables (uid)");
                eDBfree_session(ctx, session);
                return NULL;
        };


        // Register the session as logged in
        eurephiaXML_CreateDoc(ctx, 1, "Register", &loginxml, &login_n);
        assert( (loginxml != NULL) && (login_n != NULL) );
        xmlNewProp(login_n, (xmlChar *) "mode", (xmlChar *) "login");
        xmlNewProp(login_n, (xmlChar *) "uid", (xmlChar *) username);
        xmlAddChild(login_n, xmlNewText((xmlChar *) session->sessionkey));

        resxml = eDBadminAuthenticate(ctx, loginxml);
        xmlFreeDoc(loginxml);
        if( !eurephiaXML_IsResultMsg(ctx, resxml) ) {
                eurephia_log(ctx, LOG_FATAL, 0, "Could not register login");
                eDBfree_session(ctx, session);
                xmlFreeDoc(resxml);
                return NULL;
        }

        res = eurephiaXML_ParseResultMsg(ctx, resxml);
        if( res->resultType == exmlERROR ) {
                fprintf(stderr, "%s: %s\n", MODULE, res->message);
                free_nullsafe(ctx, res);
                xmlFreeDoc(resxml);
                eDBfree_session(ctx, session);
                return NULL;
        }
        xmlFreeDoc(resxml);
        free_nullsafe(ctx, res);

        return session;
}


/**
 * Validates an exisiting session key against a requested access level
 *
 * @param ctx        eurephiaCTX
 * @param sesskey    String containing the session key
 * @param accesslvl  String containing the requested access level
 *
 * @return Returns 1 if session is valid and access level is granted to the session, otherwise 0
 */
static int validate_session(eurephiaCTX *ctx, const char *sesskey, const char *accesslvl) {
        xmlDoc *sessxml = NULL, *resxml = NULL;
        xmlNode *sess_n = NULL;
        int ret = 0;

        eurephiaXML_CreateDoc(ctx, 1, "Authenticate", &sessxml, &sess_n);
        assert( (sessxml != NULL) && (sess_n != NULL) );
        xmlNewProp(sess_n, (xmlChar *) "mode", (xmlChar *) "session");

        xmlNewChild(sess_n, NULL, (xmlChar *) "sessionkey", (xmlChar *) sesskey);
        xmlNewChild(sess_n, NULL, (xmlChar *) "accesslevel", (xmlChar *) accesslvl);

        resxml = eDBadminAuthenticate(ctx, sessxml);
        xmlFreeDoc(sessxml);
        if( !eurephiaXML_IsResultMsg(ctx, resxml)  ) {
                ret = 0;
        } else {
                eurephiaRESULT *res = NULL;

                res = eurephiaXML_ParseResultMsg(ctx, resxml);
                if( res->resultType == exmlERROR ) {
                        fprintf(stderr, "%s: %s\n", MODULE, res->message);
                        ret = 0;
                } else {
                        ret = 1;
                }
                free_nullsafe(ctx, res);
        }
        if( resxml ) {
                xmlFreeDoc(resxml);
        }
        return ret;
}

/**
 * Converts a argument table (char **) into a space separated string (char *)
 *
 * @param argc argument counter
 * @param argv argument array
 *
 * @return String (char *) of all arguments, separated by space.
 */
char *args2string(int argc, char **argv) {
        char *res = NULL;
        int len = 0, i = 0;

        for( i = 0; i < argc; i++ ) {
                len += strlen_nullsafe(argv[i]) + 1;
        }

        res = (char *) malloc_nullsafe(NULL, len+2);
        assert( res != NULL );

        for( i = 0; i < argc; i++ ) {
                strcat(res, argv[i]);
                strcat(res, " ");
        }
        return res;
}


/**
 * eurephiadm main function
 *
 * @param argc argument counter
 * @param argv argument table
 *
 * @return returns 0 on success, otherwise a value > 0
 */
int main(int argc, char **argv) {
        char *logfile = NULL;
        int loglevel = 0;
        eurephiaCTX *ctx = NULL;
        eurephiaSESSION *session = NULL;
        eurephiaVALUES *cfg = NULL;
        char *sesskey_file = NULL;
        char *dbparams = NULL, *cmdargv[MAX_ARGUMENTS];
        int rc = 0, i = 0, found = 0, cmdargc = 0, argi = 0;
        eurephiadm_functions *call_fnc;

        // Global arguments we accept
        static e_options argopts[] = {
                {"--version", "-V", 0},
                {"--help", "-h", 0},
                {"--log", "-l", 1},
                {"--log-level", "-L", 1},
                {NULL, NULL, 0}
        };

        if( argc < 2 ) {
                cmd_Help(NULL, NULL, NULL, argc, argv);
                return 1;
        }

        // Parse argument line
        argi = 1;
        for( argi = 1; argi < argc; argi++ ) {
                if( *argv[argi] != '-' ) {
                        break;
                }
                switch( eurephia_getopt(&argi, argc, argv, argopts) ) {
                case 'V':
                        print_version(argv[0]);
                        return 0;

                case 'h':
                        cmd_Help(NULL, NULL, NULL, argc, argv);
                        return 0;

                case 'l':
                        logfile = optargs[0];
                        break;

                case 'L':
                        loglevel = atoi_nullsafe(optargs[0]);
                        break;

                default:
                        return 1;
                        break;
                }
        }

        if( argi >= argc ) {
                fprintf(stderr, "%s: ERROR :: No command given\n", basename(argv[0]));
                return 1;
        } else {
                for( i = argi; ((i < argc) && (i < MAX_ARGUMENTS)); i++ ) {
                        cmdargv[cmdargc++] = argv[i];
                }
        }

        // Find the command requested and save a pointer to that commands C function
        for( i = 0; cmdline_functions[i].command != NULL; i++ ) {
                call_fnc = (eurephiadm_functions *)&cmdline_functions[i];

                if( strcmp(call_fnc->command, cmdargv[0]) == 0 ) {
                        found = 1;
                        break;
                }
        }

        // Exit with error if command is not found
        if( found == 0 ) {
                fprintf(stderr, "%s:  No such command '%s'\n", basename(argv[0]), argv[argi]);
                return 5;
        }

        // Read configuration file
        cfg = ReadConfig("EUREPHIADM_CONFIG", "eurephiadm.cfg");
        if( cfg == NULL ) {
                fprintf(stderr, "No configuration file found.\n");
                return 2;
        }

        // If function do not need a logged in session, go a head process it now and exit
        if( call_fnc->need_session == 0 ) {
                rc = call_fnc->function(NULL, NULL, cfg, cmdargc, cmdargv);
                eFree_values(NULL, cfg);
                return rc;
        }

        //
        //  Load database driver, setup a context and create or reuse an open session
        //  and then call the commando's function
        //

        // Create a eurephia context and load database driver
        ctx = eurephiaCTX_init("eurephiadm", logfile, loglevel, cfg);
        if( ctx == NULL ) {
                fprintf(stderr, "Could not initialise a eurephia context.\n");
                return 3;
        }

        // Connect to the database
        dbparams = eGet_value(cfg, "database_params");
        if( !eurephia_ConnectDB(ctx, dbparams) ) {
                return 4;
        }

        // Check if we have XSLT path available, if not setup a default path
        if( eGet_value(ctx->dbc->config, "eurephiadm_xslt_path") ) {
                eAdd_value(ctx, cfg, "eurephiadm_xslt_path", eGet_value(ctx->dbc->config, "eurephiadm_xslt_path"));
        } else {
                eAdd_value(ctx, cfg, "eurephiadm_xslt_path", EUREPHIADM_XSLT_PATH);
        }

        // Load session file, if it exists.  (reuse open session)
        if( (sesskey_file = read_session_file(ctx)) == NULL ) {
                // No session file - go straight to login
                if( strcmp(call_fnc->command, "logout") == 0) {
                        fprintf(stderr, "You are already logged out\n");
                        rc = 0;
                        goto exit;
                }
                session = do_login(ctx, cfg, call_fnc->accesslvl);
        } else {
                // If we are logging out, do so here - without checking the session
                if( strcmp(call_fnc->command, "logout") == 0) {
                        char *logout_argv[] = { sesskey_file };
                        rc = cmd_Logout(ctx, NULL, cfg, 0, logout_argv);
                        remove_session_file(ctx);
                        free_nullsafe(ctx, sesskey_file);
                        goto exit;
                }

                // Session file found, check if it still is a valid session
                if( validate_session(ctx, sesskey_file, call_fnc->accesslvl) ) {;
                        // If valid, load this session
                        session = create_session(ctx, sesskey_file);
                } else {
                        // If not valid, remove session file and go to login
                        remove_session_file(ctx);
                        session = do_login(ctx, cfg, call_fnc->accesslvl);
                }
        }
        free_nullsafe(ctx, sesskey_file);


        // If we have an open session now, call the requested command
	if(session != NULL ) {
                char *argstr = NULL;

                // Write session info to a file
                if( !write_session_file(ctx, session) )  {
                        eurephia_log(ctx, LOG_ERROR, 0, "Could not write session file.  Login needed for actions");
                };

                // Execute the requested command
                rc = call_fnc->function(ctx, session, cfg, cmdargc, cmdargv);
                argstr = args2string(cmdargc, cmdargv);
                eurephia_log(ctx, LOG_INFO, 0, "Command: %s (result: %i)", argstr, rc);
                free_nullsafe(ctx, argstr);

                // Remove session info from memory
                eDBfree_session(ctx, session);
        } else {
                // If not an open session, make sure the session file is gone
                remove_session_file(ctx);
        }

 exit:
        // Disconnect from the database, and remove config and our context before exiting
        eFree_values(ctx, cfg);
        eDBdisconnect(ctx);
        eurephiaCTX_destroy(ctx);

        xmlCleanupParser();
        return rc;
}
