/*
 *
  ***** BEGIN LICENSE BLOCK *****

  Copyright (C) 2024 Olof Hagsand
                2024 Mico Micic and Moser-Baer AG

  This file is part of CLIXON.

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.

  Alternatively, the contents of this file may be used under the terms of
  the GNU General Public License Version 3 or later (the "GPL"),
  in which case the provisions of the GPL are applicable instead
  of those above. If you wish to allow use of your version of this file only
  under the terms of the GPL, and not to allow others to
  use your version of this file under the terms of Apache License version 2,
  indicate your decision by deleting the provisions above and replace them with
  the  notice and other provisions required by the GPL. If you do not delete
  the provisions above, a recipient may use your version of this file under
  the terms of any one of the Apache License version 2 or the GPL.

  ***** END LICENSE BLOCK *****
 */

#ifdef HAVE_CONFIG_H
#include "clixon_config.h" /* generated by config & autoconf */
#endif

#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>

/* cligen */
#include <cligen/cligen.h>

/* clixon */
#include <clixon/clixon.h>

/* net-snmp */
#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-includes.h>
#include <net-snmp/agent/net-snmp-agent-includes.h>

#include "snmp_lib.h"

/* SNMP v2 notification OID
 */
static oid notificationOid[] = {1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0};

struct stream_socket{
    qelem_t    sc_q;      /* queue header */
    int        sc_socket; /* socket */
};
/* Linked list of sockets used to listen for events
 */
static struct stream_socket *STREAM_SOCKETS = NULL;

/*! Read smiv2:oid for given YANG node
 *
 * @param[in]  ys       YANG node
 * @param[out] objid    oid
 * @param[out] objidlen oid length
 * @retval     0  OK
 * @retval    -1  Error
 */
int
get_oid_for_yang_node(yang_stmt *ys,
                      oid       *objid,
                      size_t    *objidlen)
{
    int        retval = -1;
    int        exist = 0;
    char      *oidstr = NULL;
    char      *path;
    yang_stmt *ytype;
    yang_stmt *ypath;
    yang_stmt *yref;
    yang_stmt *ys_to_use;

    ys_to_use = ys;

    /* Special case for leafref: Use smiv2:oid from referenced node if available. */
    if ((ytype = yang_find(ys, Y_TYPE, NULL)) != NULL &&
        strcmp(yang_argument_get(ytype), "leafref") == 0){
        if ((ypath = yang_find(ytype, Y_PATH, NULL)) == NULL){
            clixon_err(OE_YANG, 0, "No path in leafref");
            goto done;
        }
        if (yang_path_arg(ys, yang_argument_get(ypath), &yref) < 0)
            goto done;
        if (yref == NULL){
            clixon_err(OE_YANG, 0, "No referred YANG node found for leafref path %s", yang_argument_get(ypath));
            goto done;
        }
        ys_to_use = yref;
    }
    if (yang_extension_value_opt(ys_to_use, "smiv2:oid", &exist, &oidstr) < 0)
        goto done;
    if (exist == 0 || oidstr == NULL){
        clixon_debug(CLIXON_DBG_SNMP, "oid not found as SMIv2 yang extension of %s", yang_argument_get(ys));
        goto done;
    }
    if (snmp_parse_oid(oidstr, objid, objidlen) == NULL){
        clixon_err(OE_SNMP, errno, "snmp_parse_oid");
        goto done;
    }

    retval = 0;
done:
    return retval;
}

/*! Add snmp trap v2 oid to the variable list and bind the given notification oid as value.
 *
 * @param[in]  var_list   Variable list
 * @param[in]  notify_oid OID of the notification to set as value
 * @param[in]  oid_len    Length of notify_oid
 */
void
add_snmp_trapv2_oid(netsnmp_variable_list **var_list,
                    oid                    *notify_oid,
                    int                     oid_len)
{
    snmp_varlist_add_variable(
        var_list,
        notificationOid,
        OID_LENGTH(notificationOid),
        ASN_OBJECT_ID,
        (const unsigned char*)notify_oid, oid_len * sizeof(oid)
    );
}

/*! Add snmp var binding for all child nodes of cxparent.
 *
 * All child elements are read recursively. A var binding is created for each LEAF element
 * with an existing oid and added to the snmp variable list.
 *
 * @param[in]  var_list Variable list
 * @param[in]  cxparent Parent xml node
 * @param[in]  ys       YANG node
 */
int
add_snmp_var_bindings(netsnmp_variable_list **var_list,
                      cxobj                  *cxparent,
                      yang_stmt              *ys)
{
    int             retval = -1;
    int             ret;
    cxobj          *xncont = NULL; /* notification context xml */
    yang_stmt      *ychild;
    char           *xmlstr = NULL;
    char           *body = NULL;
    int             asn1type;
    char           *reason = NULL;
    unsigned char  *snmpval = NULL;
    size_t          snmplen = 0;
    oid             objid[MAX_OID_LEN] = {0,}; /* Leaf */
    size_t          objidlen = MAX_OID_LEN;

    while ((xncont = xml_child_each(cxparent, xncont, CX_ELMNT)) != NULL){
        char *node_name = xml_name(xncont);
        if ((ychild = yang_find(ys, 0, node_name)) != NULL) {
            switch (yang_keyword_get(ychild)){
                case Y_LEAF: /* only Y_LEAF type is supported here */
                    if (get_oid_for_yang_node(ychild, objid, &objidlen) < 0)
                        goto done;
                    if ((body = xml_body(xncont)) != NULL){
                        if ((ret = type_xml2snmp_pre(body, ychild, &xmlstr)) < 0)
                            goto done;
                        if (ret == 0){
                            clixon_debug(CLIXON_DBG_SNMP, "invalid data type for %s", node_name);
                            goto done;
                        }
                    }
                    if (type_yang2asn1(ychild, &asn1type, 1) < 0)
                        goto done;
                    if ((ret = type_xml2snmp(xmlstr, ychild, &asn1type, &snmpval, &snmplen, &reason)) < 0)
                        goto done;
                    if (ret == 0){
                        clixon_debug(CLIXON_DBG_SNMP, "%s", reason);
                        goto done;
                    }
                    snmp_varlist_add_variable(var_list, objid, objidlen, asn1type, snmpval, snmplen);
                    break;
                case Y_CONTAINER: /* process child nodes */
                    add_snmp_var_bindings(var_list, xncont, ychild);
                    break;
                default:
                    clixon_debug(CLIXON_DBG_SNMP, "type %s not supported in snmp trap", yang_key2str(yang_keyword_get(ychild)));
                    break;
            }
            if (xmlstr) {
                free(xmlstr);
                xmlstr = NULL;
            }
            if (snmpval) {
                free(snmpval);
                snmpval = NULL;
            }
        } else {
            clixon_debug(CLIXON_DBG_SNMP, "no yang def found for %s", node_name);
        }
    }

    retval = 0;
done:
    if (reason)
        free(reason);
    if (xmlstr)
        free(xmlstr);
    if (snmpval)
        free(snmpval);
    return retval;
}

/*! Publish snmp notification (V2 trap) for the given notification data (xml node).
 *
 * The OID is read from the YANG definition. If the notification contains data, all values
 * are bound to the corresponding OIDs and sent with the snmp trap.
 *
 * @param[in]  h   Clixon handle
 * @param[in]  xn  Notification data received from backend
 * @retval     0   OK
 * @retval    -1   Error
 */
static int
snmp_publish_notification(clixon_handle h,
                          cxobj        *xn)
{
    int                    retval = -1;
    cxobj                 *xncont = NULL;   /* notification content xml */
    yang_stmt             *ymoddata = NULL; /* notification yang module */
    yang_stmt             *yspec;
    yang_stmt             *ydef;
    oid                    objid[MAX_OID_LEN] = {0,};
    size_t                 objidlen = MAX_OID_LEN;
    netsnmp_variable_list *var_list = NULL;

    if ((yspec = clicon_dbspec_yang(h)) == NULL){
        clixon_err(OE_FATAL, 0, "No DB_SPEC");
        goto done;
    }
    while ((xncont = xml_child_each(xn, xncont, CX_ELMNT)) != NULL){
        char *node_name = xml_name(xncont);
        /* Skip eventTime. It is the only pre-defined child node as defined in RFC 5277 */
        if (strcmp(node_name, "eventTime") != 0){
            if (ys_module_by_xml(yspec, xncont, &ymoddata) < 0) /* get yang module for notification */
                goto done;
            if ((ydef = yang_find(ymoddata, Y_NOTIFICATION, node_name)) != NULL) {
                if (get_oid_for_yang_node(ydef, objid, &objidlen) < 0)
                    goto done;
                add_snmp_trapv2_oid(&var_list, objid, objidlen);
                add_snmp_var_bindings(&var_list, xncont, ydef);
                clixon_debug(CLIXON_DBG_SNMP, "sending snmp trap for %s", yang_argument_get(ydef));
                send_v2trap(var_list);
            }
        }
    }
    retval = 0;

 done:
    if (var_list)
        snmp_free_varbind(var_list);

    return retval;
}

/*! Callback when stream notifications arrive from backend
 *
 * @param[in]  s    Socket
 * @param[in]  arg  expected to be clixon_handle
 * @retval     0    OK
 * @retval    -1    Error
 */
static int
snmp_stream_cb(int   s,
               void *arg)
{
    int           retval = -1;
    clixon_handle h = (clixon_handle)arg;
    int           eof;
    cxobj        *xtop = NULL;   /* top xml */
    cxobj        *xncont = NULL; /* notification content xml */
    cxobj        *xn;            /* notification xml */
    cbuf         *cbmsg = NULL;
    int           ret;

    clixon_debug(CLIXON_DBG_SNMP, "");
    if (clixon_msg_rcv11(s, NULL, 0, &cbmsg, &eof) < 0)
        goto done;
    /* handle close from remote end: this will exit the client */
    if (eof){
        clixon_debug(CLIXON_DBG_SNMP, "eof");
        clixon_err(OE_PROTO, ESHUTDOWN, "Socket unexpected close");
        errno = ESHUTDOWN;
        clixon_exit_set(1);
        goto done;
    }
    if ((ret = clixon_xml_parse_string(cbuf_get(cbmsg), YB_NONE, NULL, &xtop, NULL)) < 0)
        goto done;
    if (ret == 0){
        clixon_err(OE_XML, EFAULT, "Invalid notification");
        goto done;
    }
    /* forward notification(s) as snmp trap */
    while ((xncont = xml_child_each(xtop, xncont, CX_ELMNT)) != NULL){
        if (strcmp(xml_name(xncont), "notification") == 0) {
            if(snmp_publish_notification(h, xncont) < 0)
                goto done;
        }
    }

 ok:
    retval = 0;
 done:
    clixon_debug(CLIXON_DBG_SNMP, "retval: %d", retval);
    if (xtop != NULL)
        xml_free(xtop);
    if (cbmsg)
        cbuf_free(cbmsg);
    return retval;
}

/*! Subscribe to all backend notifications and create streaming socket
 *
 * @param[in]  h       Clixon handle
 * @param[in]  stream  Name of the stream to subscribe
 * @param[out] socket  Socket to receive backend notifications
 * @retval     0  OK
 * @retval    -1  Error
 */
static int
snmp_stream_subscribe(clixon_handle  h,
                      char          *stream,
                      int           *socket)
{
    int     retval = -1;
    cxobj  *xret = NULL;
    cxobj  *xe;
    cbuf   *cb = NULL;
    int     s; /* socket */

    clixon_debug(CLIXON_DBG_SNMP, "Subscribing stream: %s", stream);
    *socket = -1;
    if ((cb = cbuf_new()) == NULL){
        clixon_err(OE_SNMP, errno, "cbuf_new");
        goto done;
    }
    cprintf(cb, "<rpc xmlns=\"%s\" username=\"%s\" %s><create-subscription xmlns=\"%s\"><stream>%s</stream>",
            NETCONF_BASE_NAMESPACE,
            clicon_username_get(h),
            NETCONF_MESSAGE_ID_ATTR,
            EVENT_RFC5277_NAMESPACE,
            stream);
    cprintf(cb, "</create-subscription></rpc>]]>]]>");
    if (clicon_rpc_netconf(h, cbuf_get(cb), &xret, &s) < 0)
        goto done;
    if ((xe = xpath_first(xret, NULL, "rpc-reply/rpc-error")) != NULL){
        goto done;
    }
    *socket = s;
    retval = 0;

 done:
    clixon_debug(CLIXON_DBG_SNMP, "retval: %d", retval);
    if (xret)
        xml_free(xret);
    if (cb)
        cbuf_free(cb);
    return retval;
}

int
clixon_snmp_stream_shutdown(clixon_handle h)
{
    struct stream_socket *sc;

    while ((sc = STREAM_SOCKETS) != NULL){
        DELQ(sc, STREAM_SOCKETS, struct stream_socket *);
        clixon_event_unreg_fd(sc->sc_socket, snmp_stream_cb);
        close(sc->sc_socket);
        free(sc);
    }
    return 0;
}

/*! Read all streams from backend by calling the corresponding RFC5277 get RPC
 *
 * @param[in]  h       Clixon handle
 * @param[out] streams List of stream names (malloced must be freed)
 * @param[out] count   Number of entries in streams list
 * @retval     0  OK
 * @retval    -1  Error
 */
int
get_all_streams_from_backend(clixon_handle    h,
                             char          ***streams,
                             int             *count)
{
    int     retval = -1;
    cxobj  *xret = NULL;
    cxobj  *xnchild = NULL;
    cxobj  *xe;
    cxobj  *xname;
    cbuf   *cb = NULL;
    int    cnt = 0;
    char **st = NULL;

    clixon_debug(CLIXON_DBG_SNMP, "");
    *count = 0;
    *streams = NULL;
    if ((cb = cbuf_new()) == NULL){
        clixon_err(OE_SNMP, errno, "cbuf_new");
        goto done;
    }
    /* get all streams from backend */
    cprintf(cb, "<rpc xmlns=\"%s\" username=\"%s\" %s><get><filter type=\"subtree\">"
            "<netconf xmlns=\"%s\"><streams/></netconf></filter></get></rpc>]]>]]>",
            NETCONF_BASE_NAMESPACE,
            clicon_username_get(h),
            NETCONF_MESSAGE_ID_ATTR,
            EVENT_RFC5277_NAMESPACE);
    if (clicon_rpc_netconf(h, cbuf_get(cb), &xret, NULL) < 0)
        goto done;
    if ((xe = xpath_first(xret, NULL, "rpc-reply/rpc-error")) != NULL)
        goto done;
    if ((xe = xpath_first(xret, NULL, "rpc-reply/data/netconf/streams")) == NULL) {
        clixon_debug(CLIXON_DBG_SNMP, "No streams provided by backend");
        goto ok;
    }
    while ((xnchild = xml_child_each(xe, xnchild, CX_ELMNT)) != NULL){
        if ((xname = xpath_first(xnchild, NULL, "name")) != NULL){
            char *stream_name = xml_body(xname);
            if (cnt == 0) {
                if((st = malloc(sizeof(char*))) == NULL){
                    clixon_err(OE_SNMP, errno, "malloc");
                    goto done;
                }
            }
            else{
                if((st = realloc(st, (cnt+1) * sizeof(char*))) == NULL){
                    clixon_err(OE_SNMP, errno, "realloc");
                    goto done;
                }
            }
            st[cnt] = calloc(strlen(stream_name)+1, sizeof(char));
            strcpy(st[cnt], stream_name);
            cnt++;
        }
    }

 ok:
    *count = cnt;
    *streams = st;
    retval = 0;

 done:
    clixon_debug(CLIXON_DBG_SNMP, "retval: %d", retval);
    if (xret)
        xml_free(xret);
    if (cb)
        cbuf_free(cb);
    return retval;
}

/*! Init snmp stream (traps) by subscribing to all backend notifications
 *
 * @param[in]  h  Clixon handle
 * @retval     0  OK
 * @retval    -1  Error
 * @see snmp_stream_subscribe
 */
int
clixon_snmp_stream_init(clixon_handle h)
{
    int                    retval = -1;
    struct stream_socket  *stream_socket;
    char                 **streams = NULL;
    int                    streams_num = 0;
    int                    socket = -1;

    clixon_debug(CLIXON_DBG_SNMP, "");
    if (get_all_streams_from_backend(h, &streams, &streams_num) < 0)
        goto done;
    for (int i=0; i<streams_num; i++){
        socket = -1;
        if(snmp_stream_subscribe(h, streams[i], &socket) < 0)
            goto done;
        if (socket != -1){
            /* Listen to backend socket */
            if (clixon_event_reg_fd(socket,
                                    snmp_stream_cb,
                                    h,
                                    "stream socket") < 0)
                goto done;
            if ((stream_socket = calloc(1, sizeof(struct stream_socket))) == NULL){
                clixon_err(OE_SNMP, errno, "malloc");
                goto done;
            }
            stream_socket->sc_socket = socket;
            ADDQ(stream_socket, STREAM_SOCKETS);
        }
    }

    retval = 0;

 done:
    if (streams){
        for (int i = 0; i < streams_num; i++)
            free(streams[i]);
        free(streams);
    }
    clixon_debug(CLIXON_DBG_SNMP, "retval: %d", retval);
    return retval;
}
