/*
    Authors:
        Pavel Březina <pbrezina@redhat.com>

    Copyright (C) 2015 Red Hat

    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 3 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, see <http://www.gnu.org/licenses/>.
*/

#include <talloc.h>
#include <stdlib.h>
#include <string.h>
#include <popt.h>

#include "config.h"
#include "util/util.h"
#include "confdb/confdb.h"
#include "confdb/confdb_setup.h"
#include "db/sysdb.h"
#include "tools/common/sss_tools.h"

static void sss_tool_print_common_opts(int min_len)
{
    ERROR("Help options:\n");
    fprintf(stderr, "  %-*s\t %s\n", min_len, "-?, --help",
                    _("Show this for a command"));
    fprintf(stderr, "  %-*s\t %s\n", min_len, "--usage",
                    _("Show brief usage message for a command"));
}

static struct poptOption *sss_tool_common_opts_table(void)
{
    static struct poptOption common_opts[] = {
        {"debug", '\0', POPT_ARG_INT | POPT_ARGFLAG_DOC_HIDDEN, NULL,
            0, NULL, NULL },
        POPT_TABLEEND
    };

    common_opts[0].descrip = _("The debug level to run with");

    return common_opts;
}

static void sss_tool_common_opts(struct sss_tool_ctx *tool_ctx,
                                 int *argc, const char **argv)
{
    poptContext pc;
    int debug = SSSDBG_TOOLS_DEFAULT;
    int orig_argc = *argc;
    int help = 0;

    struct poptOption options[] = {
        {"debug", '\0', POPT_ARG_INT | POPT_ARGFLAG_STRIP, &debug,
            0, _("The debug level to run with"), NULL },
        {"help", '?', POPT_ARG_VAL | POPT_ARGFLAG_DOC_HIDDEN, &help,
            1, NULL, NULL },
        POPT_TABLEEND
    };

    pc = poptGetContext(argv[0], orig_argc, argv, options, 0);
    while (poptGetNextOpt(pc) != -1) {
        /* do nothing */
    }

    /* Strip common options from arguments. We will discard_const here,
     * since it is not worth the trouble to convert it back and forth. */
    *argc = poptStrippedArgv(pc, orig_argc, discard_const_p(char *, argv));
    tool_ctx->print_help = help;

    DEBUG_CLI_INIT(debug);

    poptFreeContext(pc);
}

static errno_t sss_tool_confdb_init(TALLOC_CTX *mem_ctx,
                                    struct confdb_ctx **_confdb)
{
    struct confdb_ctx *confdb;
    char *path;
    errno_t ret;

    path = talloc_asprintf(mem_ctx, "%s/%s", DB_PATH, CONFDB_FILE);
    if (path == NULL) {
        return ENOMEM;
    }

    ret = confdb_setup(mem_ctx, path,
                       SSSD_CONFIG_FILE, CONFDB_DEFAULT_CONFIG_DIR,
                       NULL,
                       &confdb);
    talloc_zfree(path);
    if (ret != EOK) {
        DEBUG(SSSDBG_FATAL_FAILURE, "Unable to setup ConfDB [%d]: %s\n",
              ret, sss_strerror(ret));
        return ret;
    }

    if (_confdb != NULL) {
        *_confdb = confdb;
    }

    return EOK;
}

static errno_t sss_tool_domains_init(TALLOC_CTX *mem_ctx,
                                     struct confdb_ctx *confdb,
                                     struct sss_domain_info **_domains)
{
    struct sss_domain_info *domains;
    struct sss_domain_info *dom;
    errno_t ret;

    ret = confdb_expand_app_domains(confdb);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Unable to expand application domains [%d]: %s\n",
              ret, sss_strerror(ret));
        return ret;
    }

    ret = confdb_get_domains(confdb, &domains);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Unable to setup domains [%d]: %s\n",
                                   ret, sss_strerror(ret));
        return ret;
    }

    ret = sysdb_init(mem_ctx, domains);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Could not initialize connection to the sysdb\n");
        return ret;
    }

    for (dom = domains; dom != NULL;
            dom = get_next_domain(dom, SSS_GND_DESCEND)) {
        if (!IS_SUBDOMAIN(dom)) {
            /* Get flat name and domain ID (SID) from the cache
             * if available */
            ret = sysdb_master_domain_update(dom);
            if (ret != EOK) {
                DEBUG(SSSDBG_MINOR_FAILURE, "Failed to update domain %s.\n",
                                            dom->name);
            }

            /* Update list of subdomains for this domain */
            ret = sysdb_update_subdomains(dom, confdb);
            if (ret != EOK) {
                DEBUG(SSSDBG_MINOR_FAILURE,
                      "Failed to update subdomains for domain %s.\n",
                      dom->name);
            }
        }
    }

    for (dom = domains; dom != NULL;
            dom = get_next_domain(dom, SSS_GND_DESCEND)) {
        ret = sss_names_init(mem_ctx, confdb, dom->name, &dom->names);
        if (ret != EOK) {
            DEBUG(SSSDBG_CRIT_FAILURE, "sss_names_init() failed\n");
            return ret;
        }
    }

    *_domains = domains;

    return ret;
}

errno_t sss_tool_init(TALLOC_CTX *mem_ctx,
                      int *argc, const char **argv,
                      struct sss_tool_ctx **_tool_ctx)
{
    struct sss_tool_ctx *tool_ctx;

    tool_ctx = talloc_zero(mem_ctx, struct sss_tool_ctx);
    if (tool_ctx == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zero() failed\n");
        return ENOMEM;
    }

    sss_tool_common_opts(tool_ctx, argc, argv);
    *_tool_ctx = tool_ctx;

    return EOK;
}

static bool sss_tool_is_delimiter(struct sss_route_cmd *command)
{
    if (command->command != NULL && command->command[0] == '\0') {
        return true;
    }

    return false;
}

static bool sss_tools_handles_init_error(struct sss_route_cmd *command,
                                         errno_t init_err)
{
    if (init_err == EOK) {
        return true;
    }

    return command->handles_init_err == init_err;
}

static size_t sss_tool_max_length(struct sss_route_cmd *commands)
{
    size_t max = 0;
    size_t len;
    int i;

    for (i = 0; commands[i].command != NULL; i++) {
        if (sss_tool_is_delimiter(&commands[i])) {
            continue;
        }

        len = strlen(commands[i].command);
        if (max < len) {
            max = len;
        }
    }

    return max;
}

void sss_tool_usage(const char *tool_name, struct sss_route_cmd *commands)
{
    int min_len;
    int i;

    ERROR("Usage:\n%s COMMAND COMMAND-ARGS\n\n", tool_name);
    ERROR("Available commands:\n");

    min_len = sss_tool_max_length(commands);

    for (i = 0; commands[i].command != NULL; i++) {
        if (sss_tool_is_delimiter(&commands[i])) {
            fprintf(stderr, "\n%s\n", commands[i].description);
            continue;
        }

        if (commands[i].description == NULL) {
            fprintf(stderr, "* %40s\n", commands[i].command);
        } else {
            fprintf(stderr, "* %-*s\t %s\n",
                    min_len, commands[i].command, commands[i].description);
        }
    }

    ERROR("\n");
    sss_tool_print_common_opts(min_len);
}

static int tool_cmd_init(struct sss_tool_ctx *tool_ctx,
                         struct sss_route_cmd *command)
{
    int ret;

    if (command->flags & SSS_TOOL_FLAG_SKIP_CMD_INIT) {
        return EOK;
    }

    /* Connect to confdb. */
    ret = sss_tool_confdb_init(tool_ctx, &tool_ctx->confdb);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Unable to open confdb [%d]: %s\n",
              ret, sss_strerror(ret));
        goto done;
    }

    /* Setup domains. */
    ret = sss_tool_domains_init(tool_ctx, tool_ctx->confdb, &tool_ctx->domains);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Unable to setup domains [%d]: %s\n",
              ret, sss_strerror(ret));
        goto done;
    }

    ret = confdb_get_string(tool_ctx->confdb, tool_ctx,
                            CONFDB_MONITOR_CONF_ENTRY,
                            CONFDB_MONITOR_DEFAULT_DOMAIN,
                            NULL, &tool_ctx->default_domain);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Cannot get the default domain [%d]: %s\n",
              ret, strerror(ret));
        goto done;
    }

    ret = EOK;

done:
    return ret;
}

errno_t sss_tool_route(int argc, const char **argv,
                       struct sss_tool_ctx *tool_ctx,
                       struct sss_route_cmd *commands,
                       void *pvt)
{
    struct sss_cmdline cmdline;
    const char *cmd;
    int i;
    int ret;

    if (commands == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Bug: commands can't be NULL!\n");
        return EINVAL;
    }

    if (argc < 2) {
        sss_tool_usage(argv[0], commands);
        return EINVAL;
    }

    cmd = argv[1];
    for (i = 0; commands[i].command != NULL; i++) {
        if (sss_tool_is_delimiter(&commands[i])) {
            continue;
        }

        if (strcmp(commands[i].command, cmd) == 0) {
            cmdline.exec = argv[0];
            cmdline.command = argv[1];
            cmdline.argc = argc - 2;
            cmdline.argv = argv + 2;

            if (!sss_tools_handles_init_error(&commands[i], tool_ctx->init_err)) {
                DEBUG(SSSDBG_FATAL_FAILURE,
                      "Command %s does not handle initialization error [%d] %s\n",
                      cmdline.command, tool_ctx->init_err,
                      sss_strerror(tool_ctx->init_err));
                return tool_ctx->init_err;
            }

            if (!tool_ctx->print_help) {
                ret = tool_cmd_init(tool_ctx, &commands[i]);
                if (ret == ERR_SYSDB_VERSION_TOO_OLD) {
                    tool_ctx->init_err = ret;
                } else if (ret != EOK) {
                    DEBUG(SSSDBG_FATAL_FAILURE,
                          "Command initialization failed [%d] %s\n",
                          ret, sss_strerror(ret));
                    return ret;
                }
            }

            return commands[i].fn(&cmdline, tool_ctx, pvt);
        }
    }

    sss_tool_usage(argv[0], commands);
    return EINVAL;
}

static struct poptOption *nonnull_popt_table(struct poptOption *options)
{
    static struct poptOption empty[] = {
        POPT_TABLEEND
    };

    if (options == NULL) {
        return empty;
    }

    return options;
}

errno_t sss_tool_popt_ex(struct sss_cmdline *cmdline,
                         struct poptOption *options,
                         enum sss_tool_opt require_option,
                         sss_popt_fn popt_fn,
                         void *popt_fn_pvt,
                         const char *fopt_name,
                         const char *fopt_help,
                         const char **_fopt,
                         bool *_opt_set)
{
    struct poptOption opts_table[] = {
        {NULL, '\0', POPT_ARG_INCLUDE_TABLE, nonnull_popt_table(options), \
         0, _("Command options:"), NULL },
        {NULL, '\0', POPT_ARG_INCLUDE_TABLE, sss_tool_common_opts_table(), \
         0, NULL, NULL },
        POPT_AUTOHELP
        POPT_TABLEEND
    };
    const char *fopt;
    char *help;
    poptContext pc;
    bool opt_set;
    int ret;

    /* Create help option string. We always need to append command name since
     * we use POPT_CONTEXT_KEEP_FIRST. */
    if (fopt_name == NULL) {
        help = talloc_asprintf(NULL, "%s %s %s", cmdline->exec,
                               cmdline->command, _("[OPTIONS...]"));
    } else {
        help = talloc_asprintf(NULL, "%s %s %s %s", cmdline->exec,
                               cmdline->command, fopt_name, _("[OPTIONS...]"));
    }
    if (help == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf() failed\n");
        return ENOMEM;
    }

    /* Create popt context. This function is supposed to be called on
     * command argv which does not contain executable (argv[0]), therefore
     * we need to use KEEP_FIRST that ensures argv[0] is also processed. */
    pc = poptGetContext(cmdline->exec, cmdline->argc, cmdline->argv,
                        opts_table, POPT_CONTEXT_KEEP_FIRST);

    poptSetOtherOptionHelp(pc, help);

    /* Parse options. Invoke custom function if provided. If no parsing
     * function is provided, print error on unknown option. */
    while ((ret = poptGetNextOpt(pc)) != -1) {
        if (popt_fn != NULL) {
            ret = popt_fn(pc, ret, popt_fn_pvt);
            if (ret != EOK) {
                goto done;
            }
        } else {
            ERROR("Invalid option %s: %s\n\n",
                  poptBadOption(pc, 0), poptStrerror(ret));
            poptPrintHelp(pc, stderr, 0);
            ret = EINVAL;
            goto done;
        }
    }

    /* Parse free option which is always required if requested. */
    fopt = poptGetArg(pc);
    if (_fopt != NULL) {
        if (fopt == NULL) {
            ERROR("Missing option: %s\n\n", fopt_help);
            poptPrintHelp(pc, stderr, 0);
            ret = EINVAL;
            goto done;
        }

        /* No more arguments expected. If something follows it is an error. */
        if (poptGetArg(pc)) {
            ERROR("Only one free argument is expected!\n\n");
            poptPrintHelp(pc, stderr, 0);
            ret = EINVAL;
            goto done;
        }

        *_fopt = fopt;
    } else if (_fopt == NULL && fopt != NULL) {
        /* Unexpected free argument. */
        ERROR("Unexpected parameter: %s\n\n", fopt);
        poptPrintHelp(pc, stderr, 0);
        ret = EINVAL;
        goto done;
    }

    opt_set = true;
    if ((_fopt != NULL && cmdline->argc < 2) || cmdline->argc < 1) {
        opt_set = false;

        /* If at least one option is required and not provided, print error. */
        if (require_option == SSS_TOOL_OPT_REQUIRED) {
            ERROR("At least one option is required!\n\n");
            poptPrintHelp(pc, stderr, 0);
            ret = EINVAL;
            goto done;
        }
    }

    if (_opt_set != NULL) {
        *_opt_set = opt_set;
    }

    ret = EOK;

done:
    poptFreeContext(pc);
    talloc_free(help);
    return ret;
}

errno_t sss_tool_popt(struct sss_cmdline *cmdline,
                      struct poptOption *options,
                      enum sss_tool_opt require_option,
                      sss_popt_fn popt_fn,
                      void *popt_fn_pvt)
{
    return sss_tool_popt_ex(cmdline, options, require_option,
                            popt_fn, popt_fn_pvt, NULL, NULL, NULL, NULL);
}

int sss_tool_main(int argc, const char **argv,
                  struct sss_route_cmd *commands,
                  void *pvt)
{
    struct sss_tool_ctx *tool_ctx;
    uid_t uid;
    errno_t ret;

    uid = getuid();
    if (uid != 0) {
        ERROR("%1$s must be run as root\n", argv[0]);
        return EXIT_FAILURE;
    }

    ret = sss_tool_init(NULL, &argc, argv, &tool_ctx);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Unable to create tool context\n");
        return EXIT_FAILURE;
    }

    ret = sss_tool_route(argc, argv, tool_ctx, commands, pvt);
    SYSDB_VERSION_ERROR(ret);
    talloc_free(tool_ctx);
    if (ret != EOK) {
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

errno_t sss_tool_parse_name(TALLOC_CTX *mem_ctx,
                            struct sss_tool_ctx *tool_ctx,
                            const char *input,
                            const char **_username,
                            struct sss_domain_info **_domain)
{
    char *username = NULL;
    char *domname = NULL;
    struct sss_domain_info *domain;
    int ret;

    ret = sss_parse_name_for_domains(mem_ctx, tool_ctx->domains,
                                     tool_ctx->default_domain, input,
                                     &domname, &username);
    if (ret == EAGAIN) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Unable to find domain. The domain name may "
              "be a subdomain that was not yet found.\n");
        goto done;
    } else if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Unable to parse name [%d]: %s\n",
              ret, sss_strerror(ret));
        goto done;
    }

    domain = find_domain_by_name(tool_ctx->domains, domname, true);

    *_username = username;
    *_domain = domain;

    ret = EOK;

done:
    if (ret != EOK) {
        talloc_zfree(username);
        talloc_zfree(domname);
    }

    return ret;
}
