/*
    SSSD

    Authors:
        Stephen Gallagher <sgallagh@redhat.com>

    Copyright (C) 2012 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 "util/util.h"
#include "db/sysdb.h"
#include "db/sysdb_services.h"
#include "providers/ldap/sdap_async_private.h"
#include "providers/ldap/ldap_common.h"

struct sdap_get_services_state {
    struct tevent_context *ev;
    struct sdap_options *opts;
    struct sdap_handle *sh;
    struct sss_domain_info *dom;
    struct sysdb_ctx *sysdb;
    const char **attrs;
    const char *base_filter;
    char *filter;
    int timeout;
    bool enumeration;

    char *higher_usn;
    struct sysdb_attrs **services;
    size_t count;

    size_t base_iter;
    struct sdap_search_base **search_bases;
};

static errno_t
sdap_get_services_next_base(struct tevent_req *req);
static void
sdap_get_services_process(struct tevent_req *subreq);
static errno_t
sdap_save_services(TALLOC_CTX *memctx,
                   struct sysdb_ctx *sysdb,
                   struct sss_domain_info *dom,
                   struct sdap_options *opts,
                   struct sysdb_attrs **services,
                   size_t num_services,
                   char **_usn_value);
static errno_t
sdap_save_service(TALLOC_CTX *mem_ctx,
                  struct sysdb_ctx *sysdb,
                  struct sdap_options *opts,
                  struct sss_domain_info *dom,
                  struct sysdb_attrs *attrs,
                  char **_usn_value,
                  time_t now);

struct tevent_req *
sdap_get_services_send(TALLOC_CTX *memctx,
                       struct tevent_context *ev,
                       struct sss_domain_info *dom,
                       struct sysdb_ctx *sysdb,
                       struct sdap_options *opts,
                       struct sdap_search_base **search_bases,
                       struct sdap_handle *sh,
                       const char **attrs,
                       const char *filter,
                       int timeout,
                       bool enumeration)
{
    errno_t ret;
    struct tevent_req *req;
    struct sdap_get_services_state *state;

    req = tevent_req_create(memctx, &state, struct sdap_get_services_state);
    if (!req) return NULL;

    state->ev = ev;
    state->opts = opts;
    state->dom = dom;
    state->sh = sh;
    state->sysdb = sysdb;
    state->attrs = attrs;
    state->higher_usn = NULL;
    state->services =  NULL;
    state->count = 0;
    state->timeout = timeout;
    state->base_filter = filter;
    state->base_iter = 0;
    state->search_bases = search_bases;
    state->enumeration = enumeration;

    if (!state->search_bases) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Services lookup request without a search base\n");
        ret = EINVAL;
        goto done;
    }

    ret = sdap_get_services_next_base(req);

done:
    if (ret != EOK) {
        tevent_req_error(req, ret);
        tevent_req_post(req, state->ev);
    }

    return req;
}

static errno_t
sdap_get_services_next_base(struct tevent_req *req)
{
    struct tevent_req *subreq;
    struct sdap_get_services_state *state;

    state = tevent_req_data(req, struct sdap_get_services_state);

    talloc_zfree(state->filter);
    state->filter = sdap_combine_filters(state, state->base_filter,
                        state->search_bases[state->base_iter]->filter);
    if (!state->filter) {
        return ENOMEM;
    }

    DEBUG(SSSDBG_TRACE_FUNC,
          "Searching for services with base [%s]\n",
           state->search_bases[state->base_iter]->basedn);

    subreq = sdap_get_generic_send(
            state, state->ev, state->opts, state->sh,
            state->search_bases[state->base_iter]->basedn,
            state->search_bases[state->base_iter]->scope,
            state->filter, state->attrs,
            state->opts->service_map, SDAP_OPTS_SERVICES,
            state->timeout,
            state->enumeration); /* If we're enumerating, we need paging */
    if (!subreq) {
        return ENOMEM;
    }
    tevent_req_set_callback(subreq, sdap_get_services_process, req);

    return EOK;
}

static void
sdap_get_services_process(struct tevent_req *subreq)
{
    struct tevent_req *req =
            tevent_req_callback_data(subreq, struct tevent_req);
    struct sdap_get_services_state *state =
            tevent_req_data(req, struct sdap_get_services_state);
    int ret;
    size_t count, i;
    struct sysdb_attrs **services;
    bool next_base = false;

    ret = sdap_get_generic_recv(subreq, state,
                                &count, &services);
    talloc_zfree(subreq);
    if (ret) {
        tevent_req_error(req, ret);
        return;
    }

    DEBUG(SSSDBG_TRACE_FUNC,
          "Search for services, returned %zu results.\n",
           count);

    if (state->enumeration || count == 0) {
        /* No services found in this search or enumerating */
        next_base = true;
    }

    /* Add this batch of sevices to the list */
    if (count > 0) {
        state->services =
                talloc_realloc(state,
                               state->services,
                               struct sysdb_attrs *,
                               state->count + count + 1);
        if (!state->services) {
            tevent_req_error(req, ENOMEM);
            return;
        }

        /* Copy the new services into the list
         */
        for (i = 0; i < count; i++) {
            state->services[state->count + i] =
                talloc_steal(state->services, services[i]);
        }

        state->count += count;
        state->services[state->count] = NULL;
    }

    if (next_base) {
        state->base_iter++;
        if (state->search_bases[state->base_iter]) {
            /* There are more search bases to try */
            ret = sdap_get_services_next_base(req);
            if (ret != EOK) {
                tevent_req_error(req, ret);
            }
            return;
        }
    }

    /* No more search bases
     * Return ENOENT if no services were found
     */
    if (state->count == 0) {
        tevent_req_error(req, ENOENT);
        return;
    }

    ret = sdap_save_services(state, state->sysdb,
                             state->dom, state->opts,
                             state->services, state->count,
                             &state->higher_usn);
    if (ret) {
        DEBUG(SSSDBG_MINOR_FAILURE,
              "Failed to store services.\n");
        tevent_req_error(req, ret);
        return;
    }

    DEBUG(SSSDBG_TRACE_INTERNAL,
          "Saving %zu services - Done\n", state->count);

    tevent_req_done(req);
}

static errno_t
sdap_save_services(TALLOC_CTX *mem_ctx,
                   struct sysdb_ctx *sysdb,
                   struct sss_domain_info *dom,
                   struct sdap_options *opts,
                   struct sysdb_attrs **services,
                   size_t num_services,
                   char **_usn_value)
{
    errno_t ret, sret;
    time_t now;
    size_t i;
    bool in_transaction = false;
    char *higher_usn = NULL;
    char *usn_value;
    TALLOC_CTX *tmp_ctx;

    if (num_services == 0) {
        /* Nothing to do */
        return ENOENT;
    }

    tmp_ctx = talloc_new(NULL);
    if (!tmp_ctx) {
        return ENOMEM;
    }

    ret = sysdb_transaction_start(sysdb);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Failed to start transaction\n");
        goto done;
    }

    in_transaction = true;

    now = time(NULL);
    for (i = 0; i < num_services; i++) {
        usn_value = NULL;

        ret = sdap_save_service(tmp_ctx, sysdb, opts, dom,
                                services[i],
                                &usn_value, now);

        /* Do not fail completely on errors.
         * Just report the failure to save and go on */
        if (ret) {
            DEBUG(SSSDBG_MINOR_FAILURE,
                  "Failed to store service %zu. Ignoring.\n", i);
        } else {
            DEBUG(SSSDBG_TRACE_INTERNAL,
                  "Service [%zu/%zu] processed!\n", i, num_services);
        }

        if (usn_value) {
            if (higher_usn) {
                if ((strlen(usn_value) > strlen(higher_usn)) ||
                    (strcmp(usn_value, higher_usn) > 0)) {
                    talloc_zfree(higher_usn);
                    higher_usn = usn_value;
                } else {
                    talloc_zfree(usn_value);
                }
            } else {
                higher_usn = usn_value;
            }
        }
    }

    ret = sysdb_transaction_commit(sysdb);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Failed to commit transaction!\n");
        goto done;
    }
    in_transaction = false;

    if (_usn_value) {
        *_usn_value = talloc_steal(mem_ctx, higher_usn);
    }

done:
    if (in_transaction) {
        sret = sysdb_transaction_cancel(sysdb);
        if (sret != EOK) {
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "Failed to cancel transaction!\n");
        }
    }
    talloc_free(tmp_ctx);
    return ret;
}

static errno_t
sdap_save_service(TALLOC_CTX *mem_ctx,
                  struct sysdb_ctx *sysdb,
                  struct sdap_options *opts,
                  struct sss_domain_info *dom,
                  struct sysdb_attrs *attrs,
                  char **_usn_value,
                  time_t now)
{
    errno_t ret;
    TALLOC_CTX *tmp_ctx = NULL;
    struct sysdb_attrs *svc_attrs;
    struct ldb_message_element *el;
    char *usn_value = NULL;
    const char *name = NULL;
    const char **aliases;
    const char **protocols;
    const char **cased_protocols;
    const char **store_protocols;
    char **missing;
    uint16_t port;
    uint64_t cache_timeout;

    DEBUG(SSSDBG_TRACE_ALL, "Saving service\n");

    tmp_ctx = talloc_new(NULL);
    if (!tmp_ctx) {
        ret = ENOMEM;
        goto done;
    }

    svc_attrs = sysdb_new_attrs(tmp_ctx);
    if (!svc_attrs) {
        ret = ENOMEM;
        goto done;
    }

    /* Identify the primary name of this services */
    ret = sysdb_attrs_primary_name(
            sysdb, attrs,
            opts->service_map[SDAP_AT_SERVICE_NAME].name,
            &name);
    if (ret != EOK) {
        DEBUG(SSSDBG_MINOR_FAILURE,
              "Could not determine the primary name of the service\n");
        goto done;
    }

    DEBUG(SSSDBG_TRACE_INTERNAL, "Primary name: [%s]\n", name);


    /* Handle any available aliases */
    ret = sysdb_attrs_get_aliases(tmp_ctx, attrs, name,
                                  !dom->case_sensitive,
                                  &aliases);
    if (ret != EOK) {
        DEBUG(SSSDBG_MINOR_FAILURE,
              "Failed to identify service aliases\n");
        goto done;
    }

    /* Get the port number */
    ret = sysdb_attrs_get_uint16_t(attrs, SYSDB_SVC_PORT, &port);
    if (ret != EOK) {
        DEBUG(SSSDBG_MINOR_FAILURE,
              "Failed to identify service port: [%s]\n",
               strerror(ret));
        goto done;
    }

    /* Get the protocols this service offers on that port */
    ret = sysdb_attrs_get_string_array(attrs, SYSDB_SVC_PROTO,
                                       tmp_ctx, &protocols);
    if (ret != EOK) {
        DEBUG(SSSDBG_MINOR_FAILURE,
              "Failed to identify service protocols: [%s]\n",
               strerror(ret));
        goto done;
    }

    if (dom->case_sensitive == false) {
        /* Don't perform the extra mallocs if not necessary */
        ret = sss_get_cased_name_list(tmp_ctx, protocols,
                                      dom->case_sensitive, &cased_protocols);
        if (ret != EOK) {
            DEBUG(SSSDBG_MINOR_FAILURE,
                "Failed to get case_sensitive protocols names: [%s]\n",
                strerror(ret));
            goto done;
        }
    }

    store_protocols = dom->case_sensitive ? protocols : cased_protocols;

    /* Get the USN value, if available */
    ret = sysdb_attrs_get_el(attrs,
                      opts->service_map[SDAP_AT_SERVICE_USN].sys_name, &el);
    if (ret && ret != ENOENT) {
        DEBUG(SSSDBG_MINOR_FAILURE,
              "Failed to retrieve USN value: [%s]\n",
               strerror(ret));
        goto done;
    }
    if (ret == ENOENT || el->num_values == 0) {
        DEBUG(SSSDBG_TRACE_LIBS,
              "Original USN value is not available for [%s].\n",
               name);
    } else {
        ret = sysdb_attrs_add_string(svc_attrs,
                          opts->service_map[SDAP_AT_SERVICE_USN].sys_name,
                          (const char*)el->values[0].data);
        if (ret) {
            DEBUG(SSSDBG_MINOR_FAILURE,
                  "Failed to add USN value: [%s]\n",
                   strerror(ret));
            goto done;
        }
        usn_value = talloc_strdup(tmp_ctx, (const char*)el->values[0].data);
        if (!usn_value) {
            ret = ENOMEM;
            goto done;
        }
    }

    /* Make sure to remove any extra attributes from the sysdb
     * that have been removed from LDAP
     */
    ret = list_missing_attrs(svc_attrs, opts->service_map, SDAP_OPTS_SERVICES,
                             attrs, &missing);
    if (ret != EOK) {
        DEBUG(SSSDBG_MINOR_FAILURE,
              "Failed to identify removed attributes: [%s]\n",
               strerror(ret));
        goto done;
    }

    cache_timeout = dom->service_timeout;

    ret = sysdb_store_service(dom, name, port, aliases, store_protocols,
                              svc_attrs, missing, cache_timeout, now);
    if (ret != EOK) {
        DEBUG(SSSDBG_MINOR_FAILURE,
              "Failed to store service in the sysdb: [%s]\n",
               strerror(ret));
        goto done;
    }

    *_usn_value = talloc_steal(mem_ctx, usn_value);

done:
    talloc_free(tmp_ctx);
    return ret;
}

errno_t
sdap_get_services_recv(TALLOC_CTX *mem_ctx,
                       struct tevent_req *req,
                       char **usn_value)
{
    struct sdap_get_services_state *state =
            tevent_req_data(req, struct sdap_get_services_state);

    TEVENT_REQ_RETURN_ON_ERROR(req);

    if (usn_value) {
        *usn_value = talloc_steal(mem_ctx, state->higher_usn);
    }

    return EOK;
}


/* Enumeration routines */

struct enum_services_state {
    struct tevent_context *ev;
    struct sdap_id_ctx *id_ctx;
    struct sdap_id_op *op;
    struct sss_domain_info *domain;
    struct sysdb_ctx *sysdb;

    char *filter;
    const char **attrs;
};

static void
enum_services_op_done(struct tevent_req *subreq);

struct tevent_req *
enum_services_send(TALLOC_CTX *memctx,
                   struct tevent_context *ev,
                   struct sdap_id_ctx *id_ctx,
                   struct sdap_id_op *op,
                   bool purge)
{
    errno_t ret;
    struct tevent_req *req;
    struct tevent_req *subreq;
    struct enum_services_state *state;

    req = tevent_req_create(memctx, &state, struct enum_services_state);
    if (!req) return NULL;

    state->ev = ev;
    state->id_ctx = id_ctx;
    state->domain = id_ctx->be->domain;
    state->sysdb = id_ctx->be->domain->sysdb;
    state->op = op;

    if (id_ctx->srv_opts && id_ctx->srv_opts->max_service_value && !purge) {
        state->filter = talloc_asprintf(
                state,
                "(&(objectclass=%s)(%s=*)(%s=*)(%s=*)(%s>=%s)(!(%s=%s)))",
                id_ctx->opts->service_map[SDAP_OC_SERVICE].name,
                id_ctx->opts->service_map[SDAP_AT_SERVICE_NAME].name,
                id_ctx->opts->service_map[SDAP_AT_SERVICE_PORT].name,
                id_ctx->opts->service_map[SDAP_AT_SERVICE_PROTOCOL].name,
                id_ctx->opts->service_map[SDAP_AT_SERVICE_USN].name,
                id_ctx->srv_opts->max_service_value,
                id_ctx->opts->service_map[SDAP_AT_SERVICE_USN].name,
                id_ctx->srv_opts->max_service_value);
    } else {
        state->filter = talloc_asprintf(
                state,
                "(&(objectclass=%s)(%s=*)(%s=*)(%s=*))",
                id_ctx->opts->service_map[SDAP_OC_SERVICE].name,
                id_ctx->opts->service_map[SDAP_AT_SERVICE_NAME].name,
                id_ctx->opts->service_map[SDAP_AT_SERVICE_PORT].name,
                id_ctx->opts->service_map[SDAP_AT_SERVICE_PROTOCOL].name);
    }
    if (!state->filter) {
        DEBUG(SSSDBG_MINOR_FAILURE, "Failed to build base filter\n");
        ret = ENOMEM;
        goto fail;
    }

    /* TODO: handle attrs_type */
    ret = build_attrs_from_map(state, id_ctx->opts->service_map,
                               SDAP_OPTS_SERVICES, NULL,
                               &state->attrs, NULL);
    if (ret != EOK) goto fail;

    subreq = sdap_get_services_send(state, state->ev,
                                    state->domain, state->sysdb,
                                    state->id_ctx->opts,
                                    state->id_ctx->opts->sdom->service_search_bases,
                                    sdap_id_op_handle(state->op),
                                    state->attrs, state->filter,
                                    dp_opt_get_int(state->id_ctx->opts->basic,
                                                   SDAP_SEARCH_TIMEOUT),
                                    true);
    if (!subreq) {
        ret = ENOMEM;
        goto fail;
    }
    tevent_req_set_callback(subreq, enum_services_op_done, req);

    return req;

fail:
    tevent_req_error(req, ret);
    tevent_req_post(req, ev);
    return req;
}

static void
enum_services_op_done(struct tevent_req *subreq)
{
    struct tevent_req *req =
            tevent_req_callback_data(subreq, struct tevent_req);
    struct enum_services_state *state =
            tevent_req_data(req, struct enum_services_state);
    char *usn_value;
    char *endptr = NULL;
    unsigned usn_number;
    int ret;

    ret = sdap_get_services_recv(state, subreq, &usn_value);
    talloc_zfree(subreq);
    if (ret != EOK) {
        tevent_req_error(req, ret);
        return;
    }

    if (usn_value) {
        talloc_zfree(state->id_ctx->srv_opts->max_service_value);
        state->id_ctx->srv_opts->max_service_value =
                talloc_steal(state->id_ctx, usn_value);

        usn_number = strtoul(usn_value, &endptr, 10);
        if ((endptr == NULL || (*endptr == '\0' && endptr != usn_value))
            && (usn_number > state->id_ctx->srv_opts->last_usn)) {
            state->id_ctx->srv_opts->last_usn = usn_number;
        }
    }

    DEBUG(SSSDBG_FUNC_DATA, "Services higher USN value: [%s]\n",
              state->id_ctx->srv_opts->max_service_value);

    tevent_req_done(req);
}

errno_t
enum_services_recv(struct tevent_req *req)
{
    TEVENT_REQ_RETURN_ON_ERROR(req);

    return EOK;
}
