
/*
 * Copyright (C) Jan 2006 Mellanox Technologies Ltd. All rights reserved.
 * Copyright (c) 2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
 *
 * This software is available to you under a choice of one of two
 * licenses.  You may choose to be licensed under the terms of the GNU
 * General Public License (GPL) Version 2, available from the file
 * COPYING in the main directory of this source tree, or the
 * OpenIB.org BSD license below:
 *
 *     Redistribution and use in source and binary forms, with or
 *     without modification, are permitted provided that the following
 *     conditions are met:
 *
 *      - Redistributions of source code must retain the above
 *        copyright notice, this list of conditions and the following
 *        disclaimer.
 *
 *      - Redistributions in binary form must reproduce the above
 *        copyright notice, this list of conditions and the following
 *        disclaimer in the documentation and/or other materials
 *        provided with the distribution.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

#include "server_request.h"
#include <sstream>
#include <iomanip>
#include <atomic>
#include <stdlib.h>
#include <iostream>
#include <stdio.h>
#include <vector>
#include <string>

using namespace std;
#ifndef USE_CURL
#define USE_CURL
#endif

extern std::atomic<int> abort_request;

ServerRequest::ServerRequest(const char* url,
                             const char* proxy,
                             int compare_ffv,
                             bool show_progress,
                             string key,
                             string certificate,
                             unsigned int numberOfRetrials) :
    _LastErrorNo(0)
{
    _UseProxy = 0;
    _Url = url;
    _WebService = _Url + "/downloads/firmware/mlx_fw_online_query.php";
    _compare_ffv = compare_ffv;
    _show_progress = show_progress;
    _ceritifcate = certificate;
    _numberOfRetrials = numberOfRetrials;
    _headers = NULL;
    if (proxy != NULL)
    {
        if (proxy[0] != 0)
        {
            _UseProxy = 1;
            _Proxy = proxy;
        }
    }
    _key = key;
}

ServerRequest::~ServerRequest() {}

int ServerRequest::parseQueryResponse(string jsonTxt, vector<PsidQueryItem>& result, bool ignoreNotFound)
{
#ifdef USE_CURL
    string fileTag;
    Json::Value root;
    Json::CharReaderBuilder builder;
    const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
    JSONCPP_STRING errs;

    bool rc = reader->parse(jsonTxt.c_str(), jsonTxt.c_str() + jsonTxt.length(), &root, &errs);
    if (!rc)
    {
        setError(rc, std::string("-E- Failed To parse server response: ").append(errs).append("\n"));
        return -1;
    }
    result.clear();
    for (unsigned j = 0; j < root.size(); j++)
    {
        PsidQueryItem ri;
        ri.found = 0;
        ri.type = -1;
        ri.url = "";
        ri.pns = "";
        ri.isFailSafe = true; // will be used for future!
        ri.release_note = "";
        if (!root[j].isObject())
        {
            setError(-1, "-E- Failed To parse server response\n");
            return -1;
        }
        if (root[j].isMember("Found"))
        {
            if (ignoreNotFound && !root[j].get("Found", Json::nullValue).asInt())
            {
                continue;
            }
        }
        else
        {
            continue;
        }
        try
        {
            Json::Value::Members profileNames = root[j].getMemberNames();
            for (unsigned i = 0; i < profileNames.size(); i++)
            {
                if (!profileNames[i].compare("PSID"))
                {
                    ri.psid = root[j].get("PSID", Json::nullValue).asString();
                }
                else if (!profileNames[i].compare("Found"))
                {
                    ri.found = root[j].get(profileNames[i], Json::nullValue).asInt();
                }
                else if (!profileNames[i].compare("Type"))
                {
                    ri.type = root[j].get(profileNames[i], Json::nullValue).asString();
                }
                else if (!profileNames[i].compare("Found"))
                {
                    ri.found = root[j].get(profileNames[i], Json::nullValue).asInt();
                }
                else if (!profileNames[i].compare("PN"))
                {
                    ri.pns = root[j].get(profileNames[i], Json::nullValue).asString();
                }
                else if (!profileNames[i].compare("Name"))
                {
                    ri.name = root[j].get(profileNames[i], Json::nullValue).asString();
                }
                else if (!profileNames[i].compare("Release_note"))
                {
                    ri.release_note = root[j].get(profileNames[i], Json::nullValue).asString();
                }
                else if (!profileNames[i].compare("Ver"))
                {
                    const Json::Value versions = root[j]["Ver"];
                    if (versions.empty())
                    {
                        continue;
                    }
                    Json::Value::Members ROMS = versions.getMemberNames();

                    for (unsigned x = 0; x < ROMS.size(); x++)
                    {
                        string rom_type = ROMS[x];
                        string rom_ver = versions.get(ROMS[x], Json::nullValue).asString();
                        int m, n, s;
                        u_int16_t fwVer[4];
                        int len;
                        if (sscanf(rom_ver.c_str(), "%d.%d.%d", &m, &n, &s))
                        {
                            if (!_compare_ffv || rom_type != "FW")
                            {
                                fwVer[0] = m;
                                fwVer[1] = n;
                                fwVer[2] = s;
                                len = 3;
                            }
                            else
                            {
                                fwVer[0] = m;
                                fwVer[1] = n;
                                fwVer[2] = s / 100;
                                fwVer[3] = s % 100;
                                len = 4;
                            }
                            ImgVersion imgv;
                            imgv.setVersion(rom_type.c_str(), len, fwVer);
                            ri.imgVers.push_back(imgv);
                        }
                    }
                }
                else if (!profileNames[i].compare("URL"))
                {
                    ri.url = root[j].get(profileNames[i], Json::nullValue).asString();
                    ri.url = genertDownloadUrl(ri.url);
                }
            }
        }
        catch (runtime_error& e)
        {
            setError(-1, (string) "-E-" + e.what() + "\n");
            return -1;
        }
        result.push_back(ri);
        if (!ri.found)
        {
            setError(0, "-W- No online newer fw found for psid : " + ri.psid + "\n");
        }
    }
#endif
    return 0;
}

void ServerRequest::setError(int error_num, string error_msg)
{
    _LastErrorStr += error_msg;
    _LastErrorNo = error_num;
}

void ServerRequest::getError(int& error_num, string& error_msg)
{
    error_num = _LastErrorNo;
    error_msg = _LastErrorStr;
}

int ServerRequest::DownloadFilesRequest(vector<DownloadedFileProperties>& files, string os, string devs, string type)
{
    int res = 0;
#ifdef USE_CURL
    string response;
    Json::Value root;
    Json::StyledWriter writer;

    root["mode"] = 2;
    root["psids"] = "";
    root["os"] = (os.length()) ? os : "all";
    root["type"] = (type.length()) ? type : "all";
    root["devs"] = (devs.length()) ? devs : "all";
    root["key"] = _key;
    string data = writer.write(root);
    res = curl_request_str(_WebService.c_str(), data.c_str(), response);
    if (res)
    {
        return res;
    }
    res = parseDownloadFilesResponse(response, files);
    if (res)
    {
        return res;
    }

#endif
    return res;
}

int ServerRequest::parseDownloadFilesResponse(string jsonTxt, vector<DownloadedFileProperties>& files)
{
#ifdef USE_CURL
    Json::Value root;
    string url;
    Json::CharReaderBuilder builder;
    const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
    JSONCPP_STRING errs;

    bool rc = reader->parse(jsonTxt.c_str(), jsonTxt.c_str() + jsonTxt.length(), &root, &errs);
    if (!rc)
    {
        setError(rc, std::string("-E- Failed To parse server response: ").append(errs).append("\n"));
        return -1;
    }
    for (unsigned j = 0; j < root.size(); j++)
    {
        if (!root[j].isObject())
        {
            setError(-1, "-E- Failed To parse server response\n");
            return -1;
        }
        string name = "";
        string url = "";
        string descr = "";
        string release_note = "";
        string family = "";
        string file_type = "";
        string os = "";
        try
        {
            Json::Value::Members profileNames = root[j].getMemberNames();
            for (unsigned i = 0; i < profileNames.size(); i++)
            {
                if (!profileNames[i].compare("URL"))
                {
                    url = root[j].get(profileNames[i], Json::nullValue).asString();
                    url = genertDownloadUrl(url);
                }
                else if (!profileNames[i].compare("Name"))
                {
                    name = root[j].get(profileNames[i], Json::nullValue).asString();
                }
                else if (!profileNames[i].compare("Family"))
                {
                    family = root[j].get(profileNames[i], Json::nullValue).asString();
                }
                else if (!profileNames[i].compare("Type"))
                {
                    file_type = root[j].get(profileNames[i], Json::nullValue).asString();
                }
                else if (!profileNames[i].compare("Descr"))
                {
                    descr = root[j].get(profileNames[i], Json::nullValue).asString();
                }
                else if (!profileNames[i].compare("OS"))
                {
                    os = root[j].get(profileNames[i], Json::nullValue).asString();
                }
                else if (!profileNames[i].compare("Release_note"))
                {
                    release_note = root[j].get(profileNames[i], Json::nullValue).asString();
                }
            }
        }
        catch (runtime_error& e)
        {
            setError(-1, (string) "-E-" + e.what() + "\n");
            return -1;
        }
        DownloadedFileProperties file;
        file.url = url;
        file.name = name;
        file.family = family;
        file.file_type = file_type;
        file.descr = descr;
        file.os = os;
        file.release_note = release_note;
        files.push_back(file);
    }
    if (!files.size())
    {
        return -1;
    }
#endif
    return 0;
}

int ServerRequest::queryPsids(string psids, vector<PsidQueryItem>& results)
{
    int res = 0;

#ifdef USE_CURL

    Json::Value root;
    Json::StyledWriter writer;
    string response = "";
    root["psids"] = psids;
    root["mode"] = 3; // 0 query
    root["key"] = _key;
    string data = writer.write(root);
    // printf("request : %s\n", data.c_str());
    res = curl_request_str(_WebService.c_str(), data.c_str(), response);
    if (res)
    {
        return res;
    }

    // printf("Response : %s\n", response.c_str());
    res = parseQueryResponse(response, results, false);
    if (res)
    {
        return res;
    }

#endif
    return res;
}

int ServerRequest::updateMFAsRequest(vector<string>& psid_list,
                                     vector<dm_dev_id_t>& dev_types_list,
                                     vector<string>& fw_version_list,
                                     vector<PsidQueryItem>& results)
{
    int res = 0;
    (void)dev_types_list;
#ifdef USE_CURL

    Json::Value root;
    Json::StyledWriter writer;
    string psids = "";
    string devices = "";
    string response = "";
    string versions = "";
    string tmp;
    for (unsigned i = 0; i < psid_list.size(); i++)
    {
        if (i > 0)
        {
            psids += ",";
            devices += ",";
            versions += ",";
        }
        psids += psid_list[i];
        versions += fw_version_list[i];
    }

    root["psids"] = psids;
    root["devs"] = devices;
    root["mode"] = 0; // 0 update
    root["versions"] = versions;
    root["key"] = _key;
    string data = writer.write(root);
    // printf("request : %s\n", data.c_str());
    res = curl_request_str(_WebService.c_str(), data.c_str(), response);
    if (res)
    {
        return res;
    }

    // printf("Response : %s\n", response.c_str());
    res = parseQueryResponse(response, results);
    if (res)
    {
        return res;
    }

#endif
    return res;
}

string ServerRequest::genertDownloadUrl(string file_url)
{
    return _Url + "/" + file_url;
}

int ServerRequest::download(string url, string dest_path)
{
    int res = 0;
#ifdef USE_CURL
    ssize_t pos = url.rfind('/');
    string name = dest_path;
    name += "/";
    if (pos > 0)
    {
        string tmp = url.substr(pos);
        if (tmp.length() > 0)
        {
            name += tmp;
            res = curl_request(url.c_str(), NULL, name.c_str(), _numberOfRetrials);
        }
    }
#endif
    return res;
}

#ifdef USE_CURL
static size_t write_data(void* ptr, size_t size, size_t nmemb, FILE* stream)
{
    size_t written = fwrite(ptr, size, nmemb, stream);
    return written;
}
#endif

int ServerRequest::curl_request(const char* url, const char* data, const char* dest_file, int numOfRet)
{
    int res = 0;
#ifdef USE_CURL
    CURLcode rc;
    FILE* fp;
    CURL* _curl = curl_easy_init();
    if (_curl == NULL)
    {
        _LastErrorNo = ERR_SRVREQ_ACCESS_INIT;
        setError(-1, "-E- Server access error\n");
        return -1;
    }
    _LastErrorStr = "";
    fp = fopen(dest_file, "wb");
    if (fp == NULL && errno == EACCES)
    {
        unlink(dest_file);
        fp = fopen(dest_file, "wb");
    }
    if (fp == NULL)
    {
        setError(-1, "Failed to Open file for writing " + (string)strerror(errno) + "\n");
        curl_easy_cleanup(_curl);
        curl_global_cleanup();
        return -1;
    }
    curl_easy_setopt(_curl, CURLOPT_TIMEOUT, 360); // 6 minute operation timeout
    curl_easy_setopt(_curl, CURLOPT_LOW_SPEED_LIMIT, 1);
    curl_easy_setopt(_curl, CURLOPT_LOW_SPEED_TIME, 60);
    curl_easy_setopt(_curl, CURLOPT_URL, url);
    curl_easy_setopt(_curl, CURLOPT_WRITEFUNCTION, write_data);
    curl_easy_setopt(_curl, CURLOPT_WRITEDATA, fp);
    curl_easy_setopt(_curl, CURLOPT_CAINFO, _ceritifcate.c_str());
    if (_show_progress)
    {
        // Internal CURL progressmeter must be disabled if we provide our own callback
        curl_easy_setopt(_curl, CURLOPT_NOPROGRESS, false);
        // Install the callback function
        curl_easy_setopt(_curl, CURLOPT_PROGRESSFUNCTION, progress_func);
    }
    if (_Proxy.size() > 0)
    {
        curl_easy_setopt(_curl, CURLOPT_PROXY, _Proxy.c_str());
    }
    if (data != NULL)
    {
        curl_easy_setopt(_curl, CURLOPT_POST, 1);
        curl_easy_setopt(_curl, CURLOPT_POSTFIELDS, data);
    }
    curl_easy_setopt(_curl, CURLOPT_SSL_VERIFYPEER, true);

    // Perform the request, res will get the return code
    rc = curl_easy_perform(_curl);
    // Check for errors
    if (rc == CURLE_OPERATION_TIMEDOUT && numOfRet > 0)
    {
        if (curl_request(url, data, dest_file, numOfRet - 1) == 0)
        {
            rc = CURLE_OK;
        }
    }
    if (rc != CURLE_OK)
    {
        if ((rc == CURLE_OPERATION_TIMEDOUT && numOfRet == 0) || (rc != CURLE_OPERATION_TIMEDOUT))
        {
            setError(-1, (string)curl_easy_strerror(rc));
        }
        res = -1;
        goto clean_up;
    }

    res = 0;

clean_up:
    curl_easy_cleanup(_curl);
    curl_global_cleanup();
    fclose(fp);
    if (res)
    {
        unlink(dest_file);
    }
    else
    {
#if !defined(__WIN__)
        string tmp = (string)dest_file;
        if (tmp.find(".mfa") == std::string::npos && tmp.find(".exe") == std::string::npos)
        {
            int retVal = my_chmod(dest_file, EXECUTABLE_MODE);
            (void)retVal;
        }
#endif
    }
#endif
    return res;
}

#ifdef USE_CURL
static size_t writebuf_data(void* ptr, size_t size, size_t nmemb, string* strm)
{
    size_t written = size * nmemb;
    char* str = new char[written + 1];
    if (str == NULL)
    {
        return 0;
    }
    strncpy(str, (char*)ptr, written);
    str[written] = 0;
    *strm += str;
    delete[] str;
    return written;
}
#endif

int ServerRequest::curl_request_str(const char* url, const char* data, string& response)
{
    int res = 0;
#ifdef USE_CURL

    CURL* _curl = curl_easy_init();
    if (_curl == NULL)
    {
        _LastErrorNo = ERR_SRVREQ_ACCESS_INIT;
        setError(-1, "-E- Server access error\n");
        return -1;
    }

    CURLcode rc;
    curl_easy_setopt(_curl, CURLOPT_URL, url);
    curl_easy_setopt(_curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(_curl, CURLOPT_WRITEFUNCTION, writebuf_data);
    curl_easy_setopt(_curl, CURLOPT_CAINFO, _ceritifcate.c_str());

    _headers = curl_slist_append(_headers, "Accept: application/json");
    _headers = curl_slist_append(_headers, "Content-Type: application/json");
    curl_easy_setopt(_curl, CURLOPT_HTTPHEADER, _headers);
    curl_easy_setopt(_curl, CURLOPT_POST, 1);
    curl_easy_setopt(_curl, CURLOPT_POSTFIELDS, data);
    curl_easy_setopt(_curl, CURLOPT_POSTFIELDSIZE, strlen(data));
    curl_easy_setopt(_curl, CURLOPT_SSL_VERIFYPEER, true);
    // curl_easy_setopt(curl, CURLOPT_VERBOSE,1);
    // curl_easy_setopt(curl, CURLOPT_HEADER,1);

    // Perform the request, res will get the return code
    rc = curl_easy_perform(_curl);
    // fprintf(stderr, "Response : %s\n", response.c_str());
    // Check for errors
    if (rc != CURLE_OK)
    {
        setError(-1, "-E- " + (string)curl_easy_strerror(rc) + "\n");
        res = -1;
        goto clean_up;
    }
    res = 0;
clean_up:
    curl_easy_cleanup(_curl);
    curl_global_cleanup();
    curl_slist_free_all(_headers);

#endif
    return res;
}

int progress_func(void* ptr, double totalToDownload, double nowDownloaded, double totalToUpload, double nowUploaded)
{
    (void)ptr;
    (void)totalToUpload;
    (void)nowUploaded;

    double fractionDownloaded = (nowDownloaded / totalToDownload) * 100;
    fractionDownloaded = fractionDownloaded > 0 ? fractionDownloaded : 0;
    printf("\b\b\b\b%3.0f%%", fractionDownloaded);

    fflush(stdout);
    return abort_request;
}
