// SPDX-License-Identifier: LGPL-3.0-or-later

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea.
*
* nymea is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* nymea 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "jsonvalidator.h"
#include "jsonrpc/jsonhandler.h"

#include "loggingcategories.h"

#include <QJsonDocument>
#include <QColor>
#include <QDateTime>

namespace nymeaserver {

namespace {

bool hasTaggedPrefix(const QString &key, const QString &tag)
{
    int offset = 0;
    while (offset + 2 <= key.length() && key.at(offset + 1) == QLatin1Char(':')) {
        if (key.mid(offset, 2) == tag) {
            return true;
        }
        offset += 2;
    }
    return false;
}

QString stripTaggedPrefixes(QString key)
{
    while (key.length() >= 2 && key.at(1) == QLatin1Char(':')) {
        key.remove(0, 2);
    }
    return key;
}

}

bool JsonValidator::checkRefs(const QVariantMap &map, const QVariantMap &api)
{
    QVariantMap enums = api.value("enums").toMap();
    QVariantMap flags = api.value("flags").toMap();
    QVariantMap types = api.value("types").toMap();
    foreach (const QString &key, map.keys()) {
        if (map.value(key).toString().startsWith("$ref:")) {
            QString refName = map.value(key).toString().remove("$ref:");
            if (!enums.contains(refName) && !flags.contains(refName) && !types.contains(refName)) {
                qCWarning(dcJsonRpc()) << "Invalid reference to" << refName << "in" << key << map;
                return false;
            }
        }
        if (map.value(key).userType() == QMetaType::QVariantMap) {
            bool ret = checkRefs(map.value(key).toMap(), api);
            if (!ret) {
                return false;
            }
        }
        if (map.value(key).userType() == QMetaType::QVariantList) {
            foreach (const QVariant &entry, map.value(key).toList()) {
                if (entry.toString().startsWith("$ref:")) {
                    QString refName = entry.toString().remove("$ref:");
                    if (!enums.contains(refName) && !flags.contains(refName) && !types.contains(refName)) {
                        qCWarning(dcJsonRpc()) << "Invalid reference to" << refName << "in" << key << map;
                        return false;
                    }
                }
                if (entry.userType() == QMetaType::QVariantMap) {
                    bool ret = checkRefs(map.value(key).toMap(), api);
                    if (!ret) {
                        return false;
                    }
                }
            }
        }
    }
    return true;

}

JsonValidator::Result JsonValidator::validateParams(const QVariantMap &params, const QString &method, const QVariantMap &api)
{
    QVariantMap paramDefinition = api.value("methods").toMap().value(method).toMap().value("params").toMap();
    m_result = validateMap(params, paramDefinition, api, QIODevice::WriteOnly);
    m_result.setWhere(method + ", param " + m_result.where());
    return m_result;
}

JsonValidator::Result JsonValidator::validateReturns(const QVariantMap &returns, const QString &method, const QVariantMap &api)
{
    QVariantMap returnsDefinition = api.value("methods").toMap().value(method).toMap().value("returns").toMap();
    m_result = validateMap(returns, returnsDefinition, api, QIODevice::ReadOnly);
    m_result.setWhere(method + ", returns " + m_result.where());
    return m_result;
}

JsonValidator::Result JsonValidator::validateNotificationParams(const QVariantMap &params, const QString &notification, const QVariantMap &api)
{
    QVariantMap paramDefinition = api.value("notifications").toMap().value(notification).toMap().value("params").toMap();
    m_result = validateMap(params, paramDefinition, api, QIODevice::ReadOnly);
    m_result.setWhere(notification + ", param " + m_result.where());
    return m_result;
}

JsonValidator::Result JsonValidator::result() const
{
    return m_result;
}

JsonValidator::Result JsonValidator::validateMap(const QVariantMap &map, const QVariantMap &definition, const QVariantMap &api, QIODevice::OpenMode openMode)
{
    // Make sure all required values are available
    foreach (const QString &key, definition.keys()) {
        if (hasTaggedPrefix(key, QStringLiteral("o:"))) {
            continue;
        }
        if (hasTaggedPrefix(key, QStringLiteral("r:")) && openMode.testFlag(QIODevice::WriteOnly)) {
            continue;
        }
        const QString trimmedKey = stripTaggedPrefixes(key);
        if (!map.contains(trimmedKey)) {
            return Result(false, "Missing required key: " + key, key);
        }
    }

    // Make sure given values are valid
    foreach (const QString &key, map.keys()) {
        // Is the key allowed in here?
        QVariant expectedValue = definition.value(key);
        foreach (const QString &definitionKey, definition.keys()) {
            const QString normalizedDefinitionKey = stripTaggedPrefixes(definitionKey);
            if (normalizedDefinitionKey == key && definitionKey != key) {
                expectedValue = definition.value(definitionKey);
                break;
            }
        }
        if (!expectedValue.isValid()) {
            expectedValue = definition.value("o:" + key);
        }
        if (!expectedValue.isValid()) {
            expectedValue = definition.value("d:" + key);
        }
        if (!expectedValue.isValid()) {
            return Result(false, "Invalid key: " + key);
        }

        // Validate content
        QVariant value = map.value(key);

        Result result = validateEntry(value, expectedValue, api, openMode);
        if (!result.success()) {
            result.setWhere(key + '.' + result.where());
            result.setErrorString(result.errorString());
            return result;
        }
    }

    return Result(true);
}

JsonValidator::Result JsonValidator::validateEntry(const QVariant &value, const QVariant &definition, const QVariantMap &api, QIODevice::OpenMode openMode)
{
    if (definition.userType() == QMetaType::QString) {
        QString expectedTypeName = definition.toString();

        if (expectedTypeName.startsWith("$ref:")) {
            QString refName = expectedTypeName;
            refName.remove("$ref:");

            // Refs might be enums
            QVariantMap enums = api.value("enums").toMap();
            if (enums.contains(refName)) {
                QVariant refDefinition = enums.value(refName);

                QVariantList enumList = refDefinition.toList();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
                if (value.metaType().id() == QMetaType::QStringList) {
                    foreach (const QString &valueString, value.toStringList()) {
                        if (!enumList.contains(valueString)) {
                            return Result(false, "Expected enum value for " + refName + " but got " + value.toString());
                        }
                    }
                } else {
                    if (!enumList.contains(value.toString())) {
                        return Result(false, "Expected enum value for " + refName + " but got " + value.toString());
                    }
                }
#else
                if (!enumList.contains(value.toString())) {
                    return Result(false, "Expected enum value for " + refName + " but got " + value.toString());
                }
#endif
                return Result(true);
            }

            // Or flags
            QVariantMap flags = api.value("flags").toMap();
            if (flags.contains(refName)) {
                QVariant refDefinition = flags.value(refName);
                if (value.userType() != QMetaType::QVariantList && value.userType() != QMetaType::QStringList) {
                    return Result(false, "Expected flags " + refName + " but got " + value.toString());
                }
                QString flagEnum = refDefinition.toList().first().toString();
                foreach (const QVariant &flagsEntry, value.toList()) {
                    Result result = validateEntry(flagsEntry, flagEnum, api, openMode);
                    if (!result.success()) {
                        return result;
                    }
                }
                return Result(true);
            }

            QVariantMap types = api.value("types").toMap();
            QVariant refDefinition = types.value(refName);
            return validateEntry(value, refDefinition, api, openMode);
        }

        JsonHandler::BasicType expectedBasicType = JsonHandler::enumNameToValue<JsonHandler::BasicType>(expectedTypeName);

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
        // Any string converts fine to Uuid, but the resulting uuid might be null
        if (expectedBasicType == JsonHandler::Uuid && value.toUuid().isNull()) {

            QString typeName(value.typeName());
            //qCCritical(dcJsonRpc()) << value << value.userType() << value.typeId() << value.typeName() << value.toString() << value.toUuid() << value.canConvert(QMetaType::QUuid);

            // Verify if this is one of our own uuid types
            if (typeName == "ThingId" || typeName == "EventTypeId" || typeName == "StateTypeId" || typeName == "ActionTypeId") {
                return Result(true);
            }/* else {
                return Result(false, "Invalid Uuid: " + value.toString());
            }*/
        }
#else
        QMetaType::Type expectedVariantType = JsonHandler::basicTypeToMetaType(expectedBasicType);

        // Verify basic compatiblity
        if (expectedBasicType != JsonHandler::Variant && !value.canConvert(expectedVariantType)) {
            return Result(false, "Invalid value. Expected: " + definition.toString() + ", Got: " + value.toString());
        }

        // Any string converts fine to Uuid, but the resulting uuid might be null
        if (expectedBasicType == JsonHandler::Uuid && value.toUuid().isNull()) {
            return Result(false, "Invalid Uuid: " + value.toString());
        }
#endif

        // Make sure ints are valid
        if (expectedBasicType == JsonHandler::Int) {
            bool ok;
            value.toLongLong(&ok);
            if (!ok) {
                return Result(false, "Invalid Int: " + value.toString());
            }
        }
        // UInts
        if (expectedBasicType == JsonHandler::Uint) {
            bool ok;
            value.toULongLong(&ok);
            if (!ok) {
                return Result(false, "Invalid UInt: " + value.toString());
            }
        }
        // Double
        if (expectedBasicType == JsonHandler::Double) {
            bool ok;
            value.toDouble(&ok);
            if (!ok) {
                return Result(false, "Invalid Double: " + value.toString());
            }
        }
        // Color
        if (expectedBasicType == JsonHandler::Color) {
            QColor color = value.value<QColor>();
            if (!color.isValid()) {
                return Result(false, "Invalid Color: " + value.toString());
            }
        }
        // Time
        if (expectedBasicType == JsonHandler::Time) {
            QTime time = QTime::fromString(value.toString(), "hh:mm");
            if (!time.isValid()) {
                return Result(false, "Invalid Time: " + value.toString());
            }
        }


        return Result(true);
    }

    if (definition.userType() == QMetaType::QVariantMap) {
        if (value.userType() != QMetaType::QVariantMap) {
            return Result(false, "Invalid value. Expected a map but received: " + value.toString());
        }
        return validateMap(value.toMap(), definition.toMap(), api, openMode);
    }

    if (definition.userType() == QMetaType::QVariantList) {
        QVariantList list = definition.toList();
        QVariant entryDefinition = list.first();

        if (value.userType() != QMetaType::QVariantList && value.userType() != QMetaType::QStringList) {

            QString elementType = QString(value.typeName()).remove("QList<").remove(">");
            if (elementType == "ThingId" || elementType == "EventTypeId" || elementType == "StateTypeId" || elementType == "ActionTypeId") {
                elementType = "Uuid";
            } /*else if (elementType == "ParamList") {
                elementType = "Param";
            } else {
                return Result(false, "Expected list of " + entryDefinition.toString() + " but got value of type " + value.typeName() + "\n" + QJsonDocument::fromVariant(value).toJson());
            }*/
        }
        foreach (const QVariant &entry, value.toList()) {
            Result result = validateEntry(entry, entryDefinition, api, openMode);
            if (!result.success()) {
                return result;
            }
        }
        return Result(true);
    }
    Q_ASSERT_X(false, "JsonValildator", QString("Incomplete validation. Unexpected type %1 in template").arg(definition.type()).toUtf8());
    return Result(false);
}

}
