/*
 * Copyright (C) 2018-2021 QuasarApp.
 * Distributed under the lgplv3 software license, see the accompanying
 * Everyone is permitted to copy and distribute verbatim copies
 * of this license document, but changing it is not allowed.
*/

#include "accesstoken.h"
#include "databasenode.h"
#include "abstractnodeinfo.h"
#include "sqldbcache.h"
#include "sqldbwriter.h"
#include "websocketcontroller.h"
#include "asyncsqldbwriter.h"

#include <badrequest.h>
#include <quasarapp.h>
#include <websocket.h>
#include <websocketsubscriptions.h>
#include <websocketcontroller.h>
#include <QCoreApplication>
#include <ping.h>
#include <keystorage.h>
#include <basenodeinfo.h>
#include <abstractnetworkmember.h>
#include <memberpermisionobject.h>
#include <networkmember.h>
#include <deleteobject.h>
#include "dberrorcodes.h"
#include <QSet>
#include <defaultpermision.h>
#include <isubscribabledata.h>
#include <itoken.h>
#include <sqlitedbcache.h>
#include <sqldb.h>
#include <QCryptographicHash>
#include "getsinglevalue.h"
#include "setsinglevalue.h"

#define THIS_NODE "this_node_key"
namespace QH {
using namespace PKG;

DataBaseNode::DataBaseNode(QObject *ptr):
    AbstractNode(ptr) {

    _webSocketWorker = new WebSocketController(this);

    qRegisterMetaType<QSharedPointer<QH::PKG::DBObject>>();

    registerPackageType<WebSocket>();
    registerPackageType<WebSocketSubscriptions>();
    registerPackageType<DeleteObject>();


}

bool DataBaseNode::initSqlDb(QString DBparamsFile,
                             ISqlDBCache *cache,
                             SqlDBWriter *writer) {

    initDefaultDbObjects(cache, writer);

    QVariantMap params;
    _db->setSQLSources(SQLSources());

    if (DBparamsFile.isEmpty()) {
        params = defaultDbParams();

        if (!_db->init(params)) {
            return false;
        }

    } else {

        if (!_db->init(DBparamsFile)) {
            return false;
        }

    }

    if (!upgradeDataBase()) {
        QuasarAppUtils::Params::log("Failed to upgrade database",
                                    QuasarAppUtils::Error);
        return false;
    }

    QuasarAppUtils::Params::log(QString("Database loaded from: %0").arg(dbLocation()),
                                QuasarAppUtils::Debug);

    return true;
}

bool DataBaseNode::isSqlInited() const {
    return _db;
}

bool DataBaseNode::run(const QString &addres, unsigned short port) {
    if (!isSqlInited() && !initSqlDb()) {
        return false;
    }

    return AbstractNode::run(addres, port);
}

bool DataBaseNode::run(const QString &addres,
                       unsigned short port,
                       const QString &localNodeName) {

    if (localNodeName.isEmpty())
        return false;

    setLocalNodeName(localNodeName);

    if (!isSqlInited() && !initSqlDb()) {
        return false;
    }

    return AbstractNode::run(addres, port);
}

void DataBaseNode::stop() {

    AbstractNode::stop();

    if (db()) {
        auto writer = _db->writer();
        _db->softDelete();
        _db = nullptr;
        delete writer;
    }
}

DataBaseNode::~DataBaseNode() {
}

void DataBaseNode::initDefaultDbObjects(ISqlDBCache *cache,
                                        SqlDBWriter *writer) {
    if (!writer) {
        writer = new AsyncSqlDBWriter();
    }

    if (!cache) {
        cache = new SqlDB();
    }

    cache->setWriter(writer);
    _db = cache;

    connect(_db, &ISqlDBCache::sigItemChanged,
            this, &DataBaseNode::handleObjectChanged,
            Qt::DirectConnection);

    connect(_db, &ISqlDBCache::sigItemDeleted,
            this, &DataBaseNode::handleObjectDeleted,
            Qt::DirectConnection);
}


bool DataBaseNode::welcomeAddress(AbstractNodeInfo *) {
    return true;
}

bool DataBaseNode::isBanned(const QVariant &node) const {
    NetworkMember member(node);
    auto objectFromDataBase = db()->getObject<AbstractNetworkMember>(member);

    return objectFromDataBase->trust() <= 0;
}

QStringList DataBaseNode::SQLSources() const{
    return {
        DEFAULT_DB_INIT_FILE_PATH
    };
}

QSet<QString> DataBaseNode::systemTables() const {
    return {"NetworkMembers", "MemberPermisions"};
}

bool DataBaseNode::notifyObjectChanged(const QSharedPointer<PKG::ISubscribableData> &item) {

    if (!item.dynamicCast<PKG::AbstractData>()) {
        return false;
    }

    _webSocketWorker->handleItemChanged(item);

    return true;
}

void DataBaseNode::objectRemoved(const DbAddress &) {

}

void DataBaseNode::objectChanged(const QSharedPointer<DBObject> &) {

}

DBPatchMap DataBaseNode::dbPatches() const {
    return {};
}

void DataBaseNode::handleObjectChanged(const QSharedPointer<DBObject> &item) {
    notifyObjectChanged(item.staticCast<PKG::ISubscribableData>());
    objectChanged(item);

}

void DataBaseNode::handleObjectDeleted(const DbAddress &item) {
    objectRemoved(item);
}

void DataBaseNode::nodeConnected(AbstractNodeInfo *node) {
    AbstractNode::nodeConnected(node);
    welcomeAddress(node);
}

void DataBaseNode::nodeDisconnected(AbstractNodeInfo *node) {
    AbstractNode::nodeDisconnected(node);

    auto baseInfo = dynamic_cast<BaseNodeInfo*>(node);

    if (baseInfo) {
        baseInfo->reset();
    }
}

void DataBaseNode::memberSubsribed(const QVariant &, unsigned int ) {
    return;
}

void DataBaseNode::memberUnsubsribed(const QVariant &, unsigned int ) {
    return;
}

QString DataBaseNode::dbLocation() const {
    if (db() && db()->writer()) {
        return db()->writer()->databaseLocation();
    }

    return "";
}

AbstractNodeInfo *DataBaseNode::createNodeInfo(QAbstractSocket *socket, const HostAddress *clientAddress) const {
    return new BaseNodeInfo(socket, clientAddress);
}

bool DataBaseNode::changeTrust(const HostAddress &id, int diff) {    
    auto info = dynamic_cast<const BaseNodeInfo*>(getInfoPtr(id));
    if (!info)
        return false;

    if (info->id().isValid())
        return changeTrust(info->id(), diff);

    return AbstractNode::changeTrust(id, diff);
}

bool DataBaseNode::changeTrust(const QVariant &id, int diff) {
    if (!_db)
        return false;

    auto action = [diff](const QSharedPointer<DBObject> &object) {
        auto obj = object.dynamicCast<AbstractNetworkMember>();
        if (!obj) {
            return false;
        }

        obj->changeTrust(diff);

        return true;
    };

    return _db->changeObjects(NetworkMember{id}, action);
}

unsigned int DataBaseNode::sendData(const AbstractData *resp,
                            const QVariant &nodeId,
                            const Header *req) {

    auto nodes = connections();

    for (auto it = nodes.begin(); it != nodes.end(); ++it) {
        auto info = dynamic_cast<BaseNodeInfo*>(it.value());
        if (info && info->id() == nodeId) {
            return sendData(resp, info, req);
        }
    }

    return 0;
}

unsigned int DataBaseNode::sendData(const AbstractData *resp, const HostAddress &nodeId,
                            const Header *req) {
    return AbstractNode::sendData(resp, nodeId, req);

}

unsigned int DataBaseNode::sendData(const PKG::AbstractData *resp,
                                    const AbstractNodeInfo *node,
                                    const Header *req) {
    return AbstractNode::sendData(resp, node, req);
}

ParserResult DataBaseNode::parsePackage(const QSharedPointer<AbstractData> &pkg,
                                        const Header &pkgHeader,
                                        const AbstractNodeInfo *sender) {
    auto parentResult = AbstractNode::parsePackage(pkg, pkgHeader, sender);
    if (parentResult != ParserResult::NotProcessed) {
        return parentResult;
    }

    if (WebSocket::command() == pkg->cmd()) {
        WebSocket *obj = static_cast<WebSocket*>(pkg.data());

        QVariant requesterId = getSender(sender, obj);
        if (!obj->isValid()) {
            badRequest(sender->networkAddress(), pkgHeader,
                       {
                           ErrorCodes::InvalidRequest,
                           "WebSocket request is invalid"
                       });
            return ParserResult::Error;
        }

        if (!workWithSubscribe(*obj, requesterId, sender)) {
            badRequest(sender->networkAddress(), pkgHeader, {
                           ErrorCodes::InvalidRequest,
                           "WebSocket request is invalid"
                       });
            return ParserResult::Error;
        }

        return ParserResult::Processed;

    }

    return ParserResult::NotProcessed;
}

QByteArray DataBaseNode::hashgenerator(const QByteArray &pass) const {
    if (pass.isEmpty())
        return {};

    return QCryptographicHash::hash(
                QCryptographicHash::hash(pass, QCryptographicHash::Sha256) + "QuassarAppSoult",
                QCryptographicHash::Sha256);
}

ISqlDBCache *DataBaseNode::db() const {
    return _db;
}

bool DataBaseNode::workWithSubscribe(const WebSocket &rec,
                                     const QVariant &clientOrNodeid,
                                     const AbstractNodeInfo * sender) {

    auto _db = db();
    if (!_db)
        return false;

    switch (static_cast<WebSocketRequest>(rec.getRequestCmd())) {

    case WebSocketRequest::Subscribe: {
        _webSocketWorker->subscribe(clientOrNodeid, rec.subscribeId());
        memberSubsribed(clientOrNodeid, rec.subscribeId());
        return true;
    }

    case WebSocketRequest::Unsubscribe: {
        _webSocketWorker->unsubscribe(clientOrNodeid, rec.subscribeId());
        memberUnsubsribed(clientOrNodeid, rec.subscribeId());
        return true;
    }

    case WebSocketRequest::SubscribeList: {

        WebSocketSubscriptions resp;
        resp.setAddresses(_webSocketWorker->list(clientOrNodeid));

        return sendData(&resp, sender);
    }

    default: break;
    }

    return false;
}

bool DataBaseNode::isForbidenTable(const QString &table) {
    return systemTables().contains(table);
}

bool DataBaseNode::upgradeDataBase() {
    auto patches = dbPatches();
    int actyalyVersion = patches.size();
    int currentVersion = 0;

    if (!db())
        return false;

    bool fsupportUpgrade = db()->doQuery("SELECT COUNT(*) FROM DataBaseAttributes", true);

    if (!fsupportUpgrade) {

        QuasarAppUtils::Params::log("The data base of application do not support soft upgrade. "
                                    "Please remove database monyaly and restart application.",
                                    QuasarAppUtils::Error);
        return false;
    }

    PKG::GetSingleValue request({"DataBaseAttributes", "version"}, "value", "name");

    if (auto responce = _db->getObject(request)) {
        currentVersion = responce->value().toInt();
    }

    bool fUpdated = false;
    while (currentVersion < actyalyVersion) {

        auto patch = patches.value(currentVersion, {});

        QString message;
        message = "Upgrade data base!. to %0 versions";
        message = message.arg(currentVersion);

        QuasarAppUtils::Params::log(message,
                                    QuasarAppUtils::Info);

        if (patch && !patch(db())) {
            QuasarAppUtils::Params::log("Failed to " + message,
                                        QuasarAppUtils::Error);
            return false;
        }

        currentVersion++;
        fUpdated = true;
    }

    if (fUpdated) {
        auto updateVersionRequest = QSharedPointer<PKG::SetSingleValue>::create(
                    DbAddress{"DataBaseAttributes", "version"},
                    "value", currentVersion, "name");

        if (!_db->insertIfExistsUpdateObject(updateVersionRequest, true)) {
            QuasarAppUtils::Params::log("Failed to update version attribute",
                                        QuasarAppUtils::Error);
            return false;
        }
    }

    return true;
}

const QString &DataBaseNode::localNodeName() const {
    return _localNodeName;
}

void DataBaseNode::setLocalNodeName(const QString &newLocalNodeName) {
    _localNodeName = newLocalNodeName;
}

QVariantMap DataBaseNode::defaultDbParams() const {

    return {
        {"DBDriver", "QSQLITE"},
        {"DBFilePath", DEFAULT_DB_PATH + "/" + localNodeName() + "/" + localNodeName() + "_" + DEFAULT_DB_NAME},
    };
}

DBOperationResult
QH::DataBaseNode::getObject(const QVariant &requester,
                            const QH::DBObject &templateObj,
                            QSharedPointer<QH::PKG::DBObject> &result) const {

    if (!_db && !result) {
        return DBOperationResult::Unknown;
    }

    DBOperationResult permisionResult = checkPermission(requester, templateObj.dbAddress(),
                                                        Permission::Read);
    if (permisionResult != DBOperationResult::Allowed) {
        return permisionResult;
    }

    auto obj = _db->getObject(templateObj);
    if (!obj || (obj->dbAddress() != templateObj.dbAddress())) {
        return DBOperationResult::Unknown;
    }

    result = obj;
    return DBOperationResult::Allowed;
}

DBOperationResult
DataBaseNode::getObjects(const QVariant &requester,
                         const DBObject &templateObj,
                         QList<QSharedPointer<DBObject>> &result) const {
    if (!_db) {
        return DBOperationResult::Unknown;
    }

    if (!_db->getAllObjects(templateObj, result)) {
        return DBOperationResult::Unknown;
    }

    for (const auto& obj: qAsConst(result)) {
        if (!obj)
            return DBOperationResult::Unknown;

        auto permisionResult = checkPermission(requester, obj->dbAddress(),
                                               Permission::Read);
        if (permisionResult != DBOperationResult::Allowed) {
            return permisionResult;
        }
    }

    return DBOperationResult::Allowed;
}

DBOperationResult
DataBaseNode::updateObject(const QVariant &requester,
                           const QSharedPointer<DBObject> &saveObject) {

    if (!_db) {
        return DBOperationResult::Unknown;
    }

    auto permisionResult = checkPermission(requester,
                                           saveObject->dbAddress(),
                                           Permission::Write);
    if (permisionResult != DBOperationResult::Allowed) {
        return permisionResult;
    }

    if (!_db->updateObject(saveObject)) {
        return DBOperationResult::Unknown;
    }

    return DBOperationResult::Allowed;
}

DBOperationResult
DataBaseNode::createObject(const QVariant &requester,
                           const QSharedPointer<DBObject> &obj) {

    if (!_db) {
        return DBOperationResult::Unknown;
    }

    if (isForbidenTable(obj->tableName())) {
        return DBOperationResult::Forbidden;
    }

    if (!_db->insertObject(obj)) {
        return DBOperationResult::Unknown;
    }

    if (!addUpdatePermission(requester, obj->dbAddress(), Permission::Write)) {

        _db->deleteObject(obj);

        return DBOperationResult::Forbidden;
    }

    return DBOperationResult::Allowed;

}

DBOperationResult
DataBaseNode::updateIfNotExistsCreateObject(const QVariant &requester,
                                            const QSharedPointer<DBObject> &obj) {

    auto opResult = updateObject(requester, obj);
    if (opResult != QH::DBOperationResult::Unknown) {
        return opResult;
    }

    return createObject(requester, obj);
}

DBOperationResult
DataBaseNode::changeObjects(const QVariant &requester,
                            const DBObject &templateObj,
                            const std::function<bool (const QSharedPointer<DBObject> &)> &changeAction) {

    DBOperationResult result = DBOperationResult::Unknown;

    if (!_db) {
        return result;
    }

    auto execWithCheck = [this, requester, &result, &changeAction]
            (const QSharedPointer<DBObject> & obj) {

        result = checkPermission(requester, obj->dbAddress(), Permission::Update);
        if (result != DBOperationResult::Allowed) {
            return false;
        }

        return changeAction(obj);
    };

    if (!db()->changeObjects(templateObj, execWithCheck)) {
        return result;
    }

    return result;
}

QVariant DataBaseNode::getSender(const AbstractNodeInfo *connectInfo,
                                        const AbstractData *) const {

    auto info = dynamic_cast<const BaseNodeInfo*>(connectInfo);
    if (!info)
        return {};

    return info->id();
}

DBOperationResult
DataBaseNode::checkPermission(const QVariant &requester,
                              const DbAddress &objectAddress,
                              const Permission& requarimentPermision) const {

    if (!requester.isValid())
        return DBOperationResult::Unknown;

    if (!_db) {
        return DBOperationResult::Unknown;
    }

    auto member = _db->getObjectRaw(NetworkMember{requester});
    if (!member) {
        return DBOperationResult::Unknown;
    }

    auto permision =
            _db->getObject(MemberPermisionObject({requester, objectAddress}));

    if (!permision) {

        permision = _db->getObject(DefaultPermision({requester, objectAddress}));
        if (!permision)
            return DBOperationResult::Unknown;
    }

    if (permision->permisions() < requarimentPermision) {
        return DBOperationResult::Forbidden;
    }

    return DBOperationResult::Allowed;
}

bool DataBaseNode::addUpdatePermission(const QVariant &member,
                                       const DbAddress &objectAddress,
                                       const Permission &permision,
                                       const Permission &defaultPermision) const {

    if (!_db) {
        return false;
    }

    auto object = QSharedPointer<MemberPermisionObject>::create();
    object->setKey(PermisionData(member, objectAddress));
    object->setPermisions(permision);

    if (!_db->insertObject(object) && !_db->updateObject(object)) {
        return false;
    }

    auto defaultPermisionObject = QSharedPointer<DefaultPermision>::create();
    defaultPermisionObject->setKey(PermisionData({}, objectAddress));
    defaultPermisionObject->setPermisions(defaultPermision);

    if (!_db->insertObject(defaultPermisionObject) &&
            !_db->updateObject(defaultPermisionObject)) {
        return false;
    }

    return true;
}

bool DataBaseNode::removePermission(const QVariant &member,
                                    const DbAddress &objectAddress) const {

    if (!_db) {
        return false;
    }

    auto object = QSharedPointer<MemberPermisionObject>::create();
    object->setKey(PermisionData(member, objectAddress));

    if (!_db->deleteObject(object)) {
        return false;
    }

    return true;
}

DBOperationResult
DataBaseNode::deleteObject(const QVariant &requester,
                           const QSharedPointer<DBObject> &dbObject) {

    if (!_db) {
        return DBOperationResult::Unknown;
    }

    auto permisionResult = checkPermission(requester,
                                           dbObject->dbAddress(),
                                           Permission::Write);
    if (permisionResult != DBOperationResult::Allowed) {
        return permisionResult;
    }

    auto address = dbObject->dbAddress();
    if (!_db->deleteObject(dbObject)) {
        return DBOperationResult::Unknown;
    }

    return DBOperationResult::Allowed;
}

}

