/**
 * @file client.c
 * @author Roman Janota <xjanot04@fit.vutbr.cz>
 * @brief libnetconf2 client example
 *
 * @copyright
 * Copyright (c) 2022 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
 */

#include "example.h"

#include <getopt.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <libyang/libyang.h>

#include "log.h"
#include "messages_client.h"
#include "netconf.h"
#include "session_client.h"
#include "session_client_ch.h"

static void
help_print()
{
    printf("Example usage:\n"
            "    client get\n"
            "\n"
            "    Available options:\n"
            "    -h, --help\t     \tPrint usage help.\n"
            "    -p, --port\t\t<port>\tSpecify the port to connect to.\n"
            "    -u, --unix-path\t<path>\tConnect to a UNIX socket located at <path>.\n"
            "    -P, --ssh-pubkey\t<path>\tSet the path to an SSH Public key.\n"
            "    -i, --ssh-privkey\t<path>\tSet the path to an SSH Private key.\n\n"
            "    Available RPCs:\n"
            "    get [xpath-filter]\t\t\t\t\t send a <get> RPC with optional XPath filter\n"
            "    get-config [datastore] [xpath-filter]\t\t send a <get-config> RPC with optional XPath filter and datastore, the default datastore is \"running\" \n\n");
}

static enum NC_DATASTORE_TYPE
string2datastore(const char *str)
{
    if (!str) {
        return NC_DATASTORE_RUNNING;
    }

    if (!strcmp(str, "candidate")) {
        return NC_DATASTORE_CANDIDATE;
    } else if (!strcmp(str, "running")) {
        return NC_DATASTORE_RUNNING;
    } else if (!strcmp(str, "startup")) {
        return NC_DATASTORE_STARTUP;
    } else {
        return 0;
    }
}

static int
send_rpc(struct nc_session *session, NC_RPC_TYPE rpc_type, const char *param1, const char *param2)
{
    enum NC_DATASTORE_TYPE datastore;
    int r = 0, rc = 0;
    uint64_t msg_id = 0;
    struct lyd_node *envp = NULL, *op = NULL;
    struct nc_rpc *rpc = NULL;

    /* decide which type of RPC to send */
    switch (rpc_type) {
    case NC_RPC_GET:
        /* create get RPC with an optional filter */
        rpc = nc_rpc_get(param1, NC_WD_UNKNOWN, NC_PARAMTYPE_CONST);
        break;

    case NC_RPC_GETCONFIG:
        /* create get-config RPC with a source datastore and an optional filter */
        datastore = string2datastore(param1);
        if (!datastore) {
            ERR_MSG_CLEANUP("Invalid name of a datastore. Use candidate, running, startup or neither.\n");
        }
        rpc = nc_rpc_getconfig(datastore, param2, NC_WD_UNKNOWN, NC_PARAMTYPE_CONST);
        break;

    default:
        break;
    }
    if (!rpc) {
        ERR_MSG_CLEANUP("Error while creating a RPC\n");
    }

    /* send the RPC on the session and remember NETCONF message ID */
    r = nc_send_rpc(session, rpc, 100, &msg_id);
    if (r != NC_MSG_RPC) {
        ERR_MSG_CLEANUP("Couldn't send a RPC\n");
    }

    /* receive the server's reply with the expected message ID
     * as separate rpc-reply NETCONF envelopes and the parsed YANG output itself, if any */
    r = nc_recv_reply(session, rpc, msg_id, 100, &envp, &op);
    if (r != NC_MSG_REPLY) {
        ERR_MSG_CLEANUP("Couldn't receive a reply from the server\n");
    }

    /* print the whole reply */
    if (!op) {
        r = lyd_print_file(stdout, envp, LYD_XML, 0);
    } else {
        r = lyd_print_file(stdout, op, LYD_XML, 0);
        if (r) {
            ERR_MSG_CLEANUP("Couldn't print the RPC to stdout\n");
        }
        r = lyd_print_file(stdout, envp, LYD_XML, 0);
    }
    if (r) {
        ERR_MSG_CLEANUP("Couldn't print the RPC to stdout\n");
    }

cleanup:
    lyd_free_all(envp);
    lyd_free_all(op);
    nc_rpc_free(rpc);
    return rc;
}

int
main(int argc, char **argv)
{
    int rc = 0, opt, port = 0;
    struct nc_session *session = NULL;
    const char *unix_socket_path = NULL, *rpc_parameter_1 = NULL, *rpc_parameter_2 = NULL;
    const char *ssh_pubkey_path = NULL, *ssh_privkey_path = NULL;

    struct option options[] = {
        {"help",        no_argument,        NULL, 'h'},
        {"port",        required_argument,  NULL, 'p'},
        {"unix-path",   required_argument,  NULL, 'u'},
        {"ssh-pubkey",  required_argument,  NULL, 'P'},
        {"ssh-privkey", required_argument,  NULL, 'i'},
        {"debug",       no_argument,        NULL, 'd'},
        {NULL,          0,                  NULL,  0}
    };

    if (argc == 1) {
        help_print();
        goto cleanup;
    }

    /* set the path to search for schemas */
    nc_client_set_schema_searchpath(MODULES_DIR);

    opterr = 0;

    while ((opt = getopt_long(argc, argv, "hp:u:P:i:d", options, NULL)) != -1) {
        switch (opt) {
        case 'h':
            help_print();
            goto cleanup;

        case 'p':
            port = strtoul(optarg, NULL, 10);
            break;

        case 'u':
            unix_socket_path = optarg;
            break;

        case 'P':
            ssh_pubkey_path = optarg;
            break;

        case 'i':
            ssh_privkey_path = optarg;
            break;

        case 'd':
            nc_verbosity(NC_VERB_DEBUG);
            nc_libssh_thread_verbosity(2);
            break;

        default:
            ERR_MSG_CLEANUP("Invalid option or missing argument\n");
        }
    }

    if (optind == argc) {
        ERR_MSG_CLEANUP("Expected the name of RPC after options\n");
    }

    /* check invalid args combinations */
    if (unix_socket_path && port) {
        ERR_MSG_CLEANUP("Both UNIX socket path and port specified. Please choose either SSH or UNIX.\n");
    } else if (unix_socket_path && (ssh_pubkey_path || ssh_privkey_path)) {
        ERR_MSG_CLEANUP("Both UNIX socket path and a path to key(s) specified. Please choose either SSH or UNIX.\n");
    } else if ((port == 10001) && (!ssh_pubkey_path || !ssh_privkey_path)) {
        ERR_MSG_CLEANUP("You need to specify both paths to private and public keys, if you want to connect to a publickey endpoint.\n");
    } else if ((port == 10000) && (ssh_pubkey_path || ssh_privkey_path)) {
        ERR_MSG_CLEANUP("Public or private key specified, when connecting to the password endpoint.\n");
    } else if (!unix_socket_path && !port) {
        ERR_MSG_CLEANUP("Neither UNIX socket or SSH specified.\n");
    }

    /* connect to the server using the specified transport protocol */
    if (unix_socket_path) {
        /* it's UNIX socket */
        session = nc_connect_unix(unix_socket_path, NULL);
    } else {
        /* it must be SSH, so set the client SSH username to always be used when connecting to the server */
        if (nc_client_ssh_set_username(SSH_USERNAME)) {
            ERR_MSG_CLEANUP("Couldn't set the SSH username\n");
        }

        if (ssh_pubkey_path && ssh_privkey_path) {
            /* set the client's SSH keypair to be used for authentication if necessary */
            if (nc_client_ssh_add_keypair(ssh_pubkey_path, ssh_privkey_path)) {
                ERR_MSG_CLEANUP("Couldn't set client's SSH keypair.\n");
            }
        }

        /* try to connect via SSH */
        session = nc_connect_ssh(SSH_ADDRESS, port, NULL);
    }
    if (!session) {
        ERR_MSG_CLEANUP("Couldn't connect to the server\n");
    }

    /* sending a get RPC */
    if (!strcmp(argv[optind], "get")) {
        if (optind + 1 < argc) {
            /* use the specified XPath filter */
            rpc_parameter_1 = argv[optind + 1];
        }
        if (send_rpc(session, NC_RPC_GET, rpc_parameter_1, rpc_parameter_2)) {
            rc = 1;
            goto cleanup;
        }
        /* sending a get-config RPC */
    } else if (!strcmp(argv[optind], "get-config")) {
        /* use the specified datastore and optional XPath filter */
        if (optind + 2 < argc) {
            rpc_parameter_1 = argv[optind + 1];
            rpc_parameter_2 = argv[optind + 2];
        } else if (optind + 1 < argc) {
            rpc_parameter_1 = argv[optind + 1];
        }
        if (send_rpc(session, NC_RPC_GETCONFIG, rpc_parameter_1, rpc_parameter_2)) {
            rc = 1;
            goto cleanup;
        }
    } else {
        ERR_MSG_CLEANUP("Invalid name of a RPC\n");
    }

cleanup:
    nc_session_free(session, NULL);
    nc_client_destroy();
    return rc;
}