/*
    Copyright (C) 2019-Present SKALE Labs

    This file is part of sgxwallet.

    sgxwallet is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    sgxwallet 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 Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with sgxwallet.  If not, see <https://www.gnu.org/licenses/>.

    @file LevelDB.cpp
    @author Stan Kladko
    @date 2019
*/

#include <stdexcept>
#include <memory>
#include <string>
#include <iostream>

#include "leveldb/db.h"
#include <jsonrpccpp/client.h>

#include "sgxwallet_common.h"
#include "SGXException.h"
#include "LevelDB.h"

#include "ServerInit.h"

#include "third_party/spdlog/spdlog.h"
#include "common.h"

using namespace leveldb;

static WriteOptions writeOptions;
static ReadOptions readOptions;

shared_ptr<string> LevelDB::readNewStyleValue(const string& value) {
    Json::Value key_data;
    Json::Reader reader;
    reader.parse(value.c_str(), key_data);

    return std::make_shared<string>(key_data["value"].asString());
}

std::shared_ptr<string> LevelDB::readString(const string &_key) {

    auto result = std::make_shared<string>();

    CHECK_STATE(db)

    auto status = db->Get(readOptions, _key, result.get());

    throwExceptionOnError(status);

    if (status.IsNotFound()) {
        return nullptr;
    }

    if (result->at(0) == '{') {
        return readNewStyleValue(*result);
    }

    return result;
}

void LevelDB::writeString(const string &_key, const string &_value) {
    Json::Value writerData;
    writerData["value"] = _value;
    writerData["timestamp"] = std::to_string(std::time(nullptr));

    Json::FastWriter fastWriter;
    std::string output = fastWriter.write(writerData);

    auto status = db->Put(writeOptions, Slice(_key), Slice(output));

    throwExceptionOnError(status);
}

void LevelDB::deleteDHDKGKey(const string &_key) {

    string full_key = "DKG_DH_KEY_" + _key;

    auto status = db->Delete(writeOptions, Slice(full_key));

    throwExceptionOnError(status);

}

void LevelDB::deleteTempNEK(const string &_key) {

    CHECK_STATE(_key.rfind("tmp_NEK", 0) == 0);

    auto status = db->Delete(writeOptions, Slice(_key));

    throwExceptionOnError(status);
}

void LevelDB::deleteKey(const string &_key) {

    auto status = db->Delete(writeOptions, Slice(_key));

    throwExceptionOnError(status);

}

void LevelDB::throwExceptionOnError(Status _status) {
    if (_status.IsNotFound())
        return;

    if (!_status.ok()) {
        throw SGXException(COULD_NOT_ACCESS_DATABASE, ("Could not access database database:" + _status.ToString()).c_str());
    }
}

uint64_t LevelDB::visitKeys(LevelDB::KeyVisitor *_visitor, uint64_t _maxKeysToVisit) {

    CHECK_STATE(_visitor);

    uint64_t readCounter = 0;

    leveldb::Iterator *it = db->NewIterator(readOptions);
    for (it->SeekToFirst(); it->Valid(); it->Next()) {
        _visitor->visitDBKey(it->key().data());
        readCounter++;
        if (readCounter >= _maxKeysToVisit) {
            break;
        }
    }

    delete it;

    return readCounter;
}

std::vector<string> LevelDB::writeKeysToVector1(uint64_t _maxKeysToVisit){
  uint64_t readCounter = 0;
  std::vector<string> keys;

  leveldb::Iterator *it = db->NewIterator(readOptions);
  for (it->SeekToFirst(); it->Valid(); it->Next()) {
    string cur_key(it->key().data(), it->key().size());
    keys.push_back(cur_key);
    readCounter++;
    if (readCounter >= _maxKeysToVisit) {
      break;
    }
  }

  delete it;

  return keys;
}

void LevelDB::writeDataUnique(const string & name, const string &value) {
  if (readString(name)) {
    spdlog::debug("Name {} already exists", name);
    throw SGXException(KEY_SHARE_ALREADY_EXISTS, "Data with this name already exists");
  }

  writeString(name, value);
}

pair<stringstream, uint64_t> LevelDB::getAllKeys() {
    stringstream keysInfo;

    leveldb::Iterator *it = db->NewIterator(readOptions);
    uint64_t counter = 0;
    for (it->SeekToFirst(); it->Valid(); it->Next()) {
        ++counter;
        string key = it->key().ToString();
        string value;
        if (it->value().ToString()[0] == '{') {
            // new style keys
            Json::Value key_data;
            Json::Reader reader;
            reader.parse(it->value().ToString().c_str(), key_data);

            string timestamp_to_date_command = "date -d @" + key_data["timestamp"].asString();
            value = " VALUE: " + key_data["value"].asString() + ", TIMESTAMP: " + exec(timestamp_to_date_command.c_str()) + '\n';
        } else {
            // old style keys
            value = " VALUE: " + it->value().ToString();
        }
        keysInfo << "KEY: " << key << ',' << value;
    }

    return {std::move(keysInfo), counter};
}

pair<string, uint64_t> LevelDB::getLatestCreatedKey() {
    leveldb::Iterator *it = db->NewIterator(readOptions);

    int64_t latest_timestamp = 0;
    string latest_created_key_name = "";
    for (it->SeekToFirst(); it->Valid(); it->Next()) {
        if (it->value().ToString()[0] == '{') {
            // new style keys
            Json::Value key_data;
            Json::Reader reader;
            reader.parse(it->value().ToString().c_str(), key_data);

            if (std::stoi(key_data["timestamp"].asString()) > latest_timestamp) {
                latest_timestamp = std::stoi(key_data["timestamp"].asString());
                latest_created_key_name = it->key().ToString();
            }
        } else {
            // old style keys
            // assuming server has at least one new-style key created
            continue;
        }
    }

    return {latest_created_key_name, latest_timestamp};
}


LevelDB::LevelDB(string &filename) {
    leveldb::Options options;
    options.create_if_missing = true;

    if (!leveldb::DB::Open(options, filename, (leveldb::DB **) &db).ok()) {
        throw std::runtime_error("Unable to open levelDB database");
    }

    if (db == nullptr) {
        throw std::runtime_error("Null levelDB object");
    }
}

LevelDB::~LevelDB() {
}

const std::shared_ptr<LevelDB> &LevelDB::getLevelDb() {
    CHECK_STATE(levelDb)
    return levelDb;
}

const std::shared_ptr<LevelDB> &LevelDB::getCsrDb() {
    CHECK_STATE(csrDb)
    return csrDb;
}

const std::shared_ptr<LevelDB> &LevelDB::getCsrStatusDb() {
    CHECK_STATE(csrStatusDb)
    return csrStatusDb;
}


std::shared_ptr<LevelDB> LevelDB::levelDb = nullptr;

std::shared_ptr<LevelDB> LevelDB::csrDb = nullptr;

std::shared_ptr<LevelDB> LevelDB::csrStatusDb = nullptr;

string LevelDB::sgx_data_folder;

bool LevelDB::isInited = false;

void LevelDB::initDataFolderAndDBs() {
    CHECK_STATE(!isInited)
    isInited = true;

    spdlog::info("Initing wallet database ... ");

    char cwd[PATH_MAX];

    if (getcwd(cwd, sizeof(cwd)) == NULL) {
        spdlog::error("Could not get current working directory.");
        throw SGXException(COULD_NOT_GET_WORKING_DIRECTORY, "Could not get current working directory.");
    }

    sgx_data_folder = string(cwd) + "/" + SGXDATA_FOLDER;

    struct stat info;
    if (stat(sgx_data_folder.c_str(), &info) !=0 ){
        spdlog::info("sgx_data folder does not exist. Creating ...");

        if (system(("mkdir " + sgx_data_folder).c_str()) == 0){
            spdlog::info("Successfully created sgx_data folder");
        }
        else{
            spdlog::error("Could not create sgx_data folder.");
            throw SGXException(ERROR_CREATING_SGX_DATA_FOLDER, "Could not create sgx_data folder.");
        }
    }

    spdlog::info("Opening wallet databases");

    auto dbName = sgx_data_folder +  WALLETDB_NAME;
    levelDb = make_shared<LevelDB>(dbName);

    auto csr_dbname = sgx_data_folder + "CSR_DB";
    csrDb = make_shared<LevelDB>(csr_dbname);

    auto csr_status_dbname = sgx_data_folder + "CSR_STATUS_DB";
    csrStatusDb = make_shared<LevelDB>(csr_status_dbname);

    spdlog::info("Successfully opened databases");
}

const string &LevelDB::getSgxDataFolder() {
    return sgx_data_folder;
}