/**
 * @file netconf_nmda.c
 * @author Michal Vasko <mvasko@cesnet.cz>
 * @brief ietf-netconf-nmda callbacks
 *
 * @copyright
 * Copyright (c) 2019 - 2021 Deutsche Telekom AG.
 * Copyright (c) 2017 - 2021 CESNET, z.s.p.o.
 *
 * This source code is licensed under BSD 3-Clause License (the "License").
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://opensource.org/licenses/BSD-3-Clause
 */

#define _GNU_SOURCE

#include "netconf_nmda.h"

#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <libyang/libyang.h>
#include <sysrepo.h>

#include "common.h"
#include "compat.h"
#include "config.h"
#include "log.h"
#include "netconf_acm.h"

/**
 * @brief Perform origin filtering.
 *
 * @param[in,out] data Data to filter.
 * @param[in] filter Origin filter identity.
 * @param[in] negated Whether the filter is negated.
 * @return Sysrepo error value.
 */
static int
op_data_filter_origin(struct lyd_node **data, const struct lysc_ident *filter, int negated)
{
    struct ly_set *set;
    struct lyd_node *node;
    char *xpath;
    int ret;
    LY_ERR lyrc;
    uint32_t i;

    if (!*data) {
        return SR_ERR_OK;
    }

    if (negated) {
        ret = asprintf(&xpath, "//*[@origin and derived-from-or-self(@origin, '%s:%s')]", filter->module->name, filter->name);
    } else {
        ret = asprintf(&xpath, "//*[@origin and not(derived-from-or-self(@origin, '%s:%s'))]", filter->module->name, filter->name);
    }
    if (ret == -1) {
        EMEM;
        return SR_ERR_NO_MEMORY;
    }

    lyrc = lyd_find_xpath(*data, xpath, &set);
    free(xpath);
    if (lyrc) {
        return SR_ERR_INTERNAL;
    }

    if (set->count) {
        /* go backwards to allow safe node freeing */
        i = set->count;
        do {
            --i;
            node = set->dnodes[i];
            if (node->schema->flags & LYS_CONFIG_R) {
                /* state nodes are not affected */
                continue;
            }

            /* free non-matching subtree */
            if (node == *data) {
                *data = (*data)->next;
            }
            lyd_free_tree(node);
        } while (i);
    }
    ly_set_free(set, NULL);

    return SR_ERR_OK;
}

int
np2srv_rpc_getdata_cb(sr_session_ctx_t *session, uint32_t UNUSED(sub_id), const char *UNUSED(op_path),
        const struct lyd_node *input, sr_event_t event, uint32_t UNUSED(request_id), struct lyd_node *output,
        void *UNUSED(private_data))
{
    struct lyd_node_term *leaf;
    struct lyd_node *node, *select_data = NULL, *data = NULL;
    struct np2_filter filter = {0};
    int rc = SR_ERR_OK;
    struct np2_user_sess *user_sess = NULL;
    uint32_t i, max_depth = 0;
    struct ly_set *nodeset;
    sr_datastore_t ds;
    NC_WD_MODE nc_wd;
    sr_get_oper_options_t get_opts = 0;
    const char *username;

    if (NP_IGNORE_RPC(session, event)) {
        /* ignore in this case */
        return SR_ERR_OK;
    }

    /* get default value for with-defaults */
    nc_server_get_capab_withdefaults(&nc_wd, NULL);

    /* get know which datastore is being affected */
    lyd_find_path(input, "datastore", 0, (struct lyd_node **)&leaf);
    if (!strcmp(leaf->value.ident->name, "running")) {
        ds = SR_DS_RUNNING;
    } else if (!strcmp(leaf->value.ident->name, "startup")) {
        ds = SR_DS_STARTUP;
    } else if (!strcmp(leaf->value.ident->name, "candidate")) {
        ds = SR_DS_CANDIDATE;
    } else if (!strcmp(leaf->value.ident->name, "operational")) {
        ds = SR_DS_OPERATIONAL;
    } else {
        rc = SR_ERR_INVAL_ARG;
        sr_session_set_error_message(session, "Datastore \"%s\" is not supported.", lyd_get_value(&leaf->node));
        goto cleanup;
    }

    /* create filters */
    lyd_find_xpath(input, "subtree-filter | xpath-filter", &nodeset);
    node = nodeset->count ? nodeset->dnodes[0] : NULL;
    ly_set_free(nodeset, NULL);
    if (node && !strcmp(node->schema->name, "subtree-filter")) {
        if (op_filter_subtree2xpath(((struct lyd_node_any *)node)->value.tree, &filter)) {
            rc = SR_ERR_INTERNAL;
            goto cleanup;
        }
    } else {
        filter.filters = malloc(sizeof *filter.filters);
        if (!filter.filters) {
            EMEM;
            rc = SR_ERR_NO_MEMORY;
            goto cleanup;
        }
        filter.count = 1;
        filter.filters[0].str = node ? strdup(lyd_get_value(node)) : strdup("/*");
        if (!filter.filters[0].str) {
            EMEM;
            rc = SR_ERR_NO_MEMORY;
            goto cleanup;
        }
        filter.filters[0].selection = 1;
    }

    /* config filter */
    lyd_find_path(input, "config-filter", 0, &node);
    if (node) {
        if (!strcmp(lyd_get_value(node), "false")) {
            get_opts |= SR_OPER_NO_CONFIG;
        } else {
            get_opts |= SR_OPER_NO_STATE;
        }
    }

    /* depth */
    lyd_find_path(input, "max-depth", 0, &node);
    if (node && strcmp(lyd_get_value(node), "unbounded")) {
        max_depth = ((struct lyd_node_term *)node)->value.uint16;
    }

    /* origin */
    lyd_find_path(input, "with-origin", 0, &node);
    if (node) {
        get_opts |= SR_OPER_WITH_ORIGIN;
    }

    /* get with-defaults mode */
    lyd_find_path(input, "with-defaults", 0, &node);
    if (node) {
        if (!strcmp(lyd_get_value(node), "report-all")) {
            nc_wd = NC_WD_ALL;
        } else if (!strcmp(lyd_get_value(node), "report-all-tagged")) {
            nc_wd = NC_WD_ALL_TAG;
        } else if (!strcmp(lyd_get_value(node), "trim")) {
            nc_wd = NC_WD_TRIM;
        } else {
            assert(!strcmp(lyd_get_value(node), "explicit"));
            nc_wd = NC_WD_EXPLICIT;
        }
    }

    /* get the user session */
    if ((rc = np_get_user_sess(session, NULL, &user_sess))) {
        goto cleanup;
    }

    /* update sysrepo session datastore */
    sr_session_switch_ds(user_sess->sess, ds);

    /*
     * create the data tree for the data reply
     */
    if ((rc = op_filter_data_get(user_sess->sess, max_depth, get_opts, &filter, session, &select_data))) {
        goto cleanup;
    }
    if ((rc = op_filter_data_filter(&select_data, &filter, 0, &data))) {
        goto cleanup;
    }

    /* origin filter */
    lyd_find_xpath(input, "origin-filter | negated-origin-filter", &nodeset);
    for (i = 0; i < nodeset->count; ++i) {
        leaf = (struct lyd_node_term *)nodeset->dnodes[i];
        op_data_filter_origin(&data, leaf->value.ident, strcmp(leaf->schema->name, "origin-filter"));
    }
    ly_set_free(nodeset, NULL);

    /* perform correct NACM filtering */
    sr_session_get_orig_data(session, 1, NULL, (const void **)&username);
    ncac_check_data_read_filter(&data, username);

    /* add output */
    if (lyd_new_any(output, NULL, "data", data, 1, LYD_ANYDATA_DATATREE, 1, NULL)) {
        goto cleanup;
    }
    data = NULL;

    /* success */

cleanup:
    op_filter_erase(&filter);
    lyd_free_siblings(select_data);
    lyd_free_siblings(data);
    np_release_user_sess(user_sess);
    return rc;
}

int
np2srv_rpc_editdata_cb(sr_session_ctx_t *session, uint32_t UNUSED(sub_id), const char *UNUSED(op_path),
        const struct lyd_node *input, sr_event_t event, uint32_t UNUSED(request_id), struct lyd_node *UNUSED(output),
        void *UNUSED(private_data))
{
    sr_datastore_t ds;
    struct ly_set *nodeset;
    struct lyd_node_term *leaf;
    struct lyd_node *node, *config = NULL;
    struct np2_user_sess *user_sess = NULL;
    const char *defop;
    int rc = SR_ERR_OK;

    if (NP_IGNORE_RPC(session, event)) {
        /* ignore in this case */
        return SR_ERR_OK;
    }

    /* get know which datastore is being affected */
    lyd_find_path(input, "datastore", 0, (struct lyd_node **)&leaf);
    if (!strcmp(leaf->value.ident->name, "running")) {
        ds = SR_DS_RUNNING;
    } else if (!strcmp(leaf->value.ident->name, "startup")) {
        ds = SR_DS_STARTUP;
    } else if (!strcmp(leaf->value.ident->name, "candidate")) {
        ds = SR_DS_CANDIDATE;
    } else {
        rc = SR_ERR_INVAL_ARG;
        sr_session_set_error_message(session, "Datastore \"%s\" is not supported or writable.", lyd_get_value(&leaf->node));
        goto cleanup;
    }

    /* default-operation */
    lyd_find_path(input, "default-operation", 0, &node);
    defop = lyd_get_value(node);

    /* config */
    lyd_find_xpath(input, "config | url", &nodeset);
    node = nodeset->dnodes[0];
    ly_set_free(nodeset, NULL);
    if (!strcmp(node->schema->name, "config")) {
        config = op_parse_config((struct lyd_node_any *)node, LYD_PARSE_OPAQ | LYD_PARSE_ONLY, &rc, session);
        if (rc) {
            goto cleanup;
        }
    } else {
        assert(!strcmp(node->schema->name, "url"));
#ifdef NP2SRV_URL_CAPAB
        config = op_parse_url(lyd_get_value(node), LYD_PARSE_OPAQ | LYD_PARSE_ONLY, &rc, session);
        if (rc) {
            goto cleanup;
        }
#else
        rc = SR_ERR_UNSUPPORTED;
        sr_session_set_error_message(session, "URL not supported.");
        goto cleanup;
#endif
    }

    /* get the user session */
    if ((rc = np_get_user_sess(session, NULL, &user_sess))) {
        goto cleanup;
    }

    /* update sysrepo session datastore */
    sr_session_switch_ds(user_sess->sess, ds);

    /* sysrepo API */
    rc = sr_edit_batch(user_sess->sess, config, defop);
    if (rc != SR_ERR_OK) {
        goto cleanup;
    }

    rc = sr_apply_changes(user_sess->sess, np2srv.sr_timeout);
    if (rc != SR_ERR_OK) {
        sr_session_dup_error(user_sess->sess, session);
        goto cleanup;
    }

    /* success */

cleanup:
    lyd_free_siblings(config);
    np_release_user_sess(user_sess);
    return rc;
}
