/*
 * Copyright (C) 2019-2025 Dmitriy Purgin <dmitriy.purgin@sequality.at>
 * Copyright (C) 2019-2025 sequality software engineering e.U. <office@sequality.at>
 *
 * This file is part of QtOrm library.
 *
 * QtOrm 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.
 *
 * QtOrm 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 QtOrm.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "qormsqlitestatementgenerator_p.h"
#include "qormfilter.h"
#include "qormfilterexpression.h"
#include "qormglobal_p.h"
#include "qormmetadata.h"
#include "qormmetadatacache.h"
#include "qormorder.h"
#include "qormquery.h"
#include "qormrelation.h"

#include <QtCore/qstringbuilder.h>

QT_BEGIN_NAMESPACE

[[nodiscard]] static QString insertParameter(QVariantMap& boundParameters,
                                             QString name,
                                             QVariant value)
{
    QString key = ':' % name;

    for (int i = 0; boundParameters.contains(key); ++i)
        key = ':' % name % QString::number(i);

    boundParameters.insert(key, value);

    return key;
}

[[nodiscard]] static QVariant propertyValueForQuery(const QObject* entityInstance,
                                                    const QOrmPropertyMapping& propertyMapping)
{
    if (propertyMapping.isReference() && !propertyMapping.isTransient())
    {
        const QOrmMetadata* referencedEntity = propertyMapping.referencedEntity();

        Q_ASSERT(referencedEntity != nullptr);
        Q_ASSERT(referencedEntity->objectIdMapping() != nullptr);

        const QObject* referencedInstance =
            QOrmPrivate::propertyValue(entityInstance, propertyMapping.classPropertyName())
                .value<QObject*>();

        return referencedInstance == nullptr
                   ? QVariant::fromValue(nullptr)
                   : QOrmPrivate::objectIdPropertyValue(referencedInstance, *referencedEntity);
    }
    else
    {
        return QOrmPrivate::propertyValue(entityInstance, propertyMapping.classPropertyName());
    }
}

QOrmSqliteStatementGenerator::QOrmSqliteStatementGenerator()
{
}

QOrmSqliteStatementGenerator::QOrmSqliteStatementGenerator(Options options)
    : m_options{options}
{
}

std::pair<QString, QVariantMap> QOrmSqliteStatementGenerator::generate(const QOrmQuery& query)
{
    QVariantMap boundParameters;
    QString statement = generate(query, boundParameters);

    return std::make_pair(statement, boundParameters);
}

QString QOrmSqliteStatementGenerator::generate(const QOrmQuery& query, QVariantMap& boundParameters)
{
    switch (query.operation())
    {
        case QOrm::Operation::Create:
            return generateInsertStatement(*query.relation().mapping(),
                                           query.entityInstance(),
                                           boundParameters);

        case QOrm::Operation::Update:
            return generateUpdateStatement(*query.relation().mapping(),
                                           query.entityInstance(),
                                           boundParameters);

        case QOrm::Operation::Read:
        case QOrm::Operation::Count:
            return generateSelectStatement(query, boundParameters);

        case QOrm::Operation::Delete:
            Q_ASSERT(query.relation().type() == QOrm::RelationType::Mapping);

            if (query.entityInstance() != nullptr)
            {
                return generateDeleteStatement(*query.relation().mapping(),
                                               query.entityInstance(),
                                               boundParameters);
            }
            else if (query.expressionFilter().has_value())
            {
                return generateDeleteStatement(*query.relation().mapping(),
                                               *query.expressionFilter(),
                                               boundParameters);
            }
            else
                Q_ORM_UNEXPECTED_STATE;

        default:
            Q_ORM_UNEXPECTED_STATE;
    }
}

QString QOrmSqliteStatementGenerator::generateInsertStatement(const QOrmMetadata& relation,
                                                              const QObject* entityInstance,
                                                              QVariantMap& boundParameters)
{
    QStringList fieldsList;
    QStringList valuesList;

    for (const QOrmPropertyMapping& propertyMapping : relation.propertyMappings())
    {
        if (propertyMapping.isAutogenerated() || propertyMapping.isTransient())
            continue;

        QVariant propertyValue = propertyValueForQuery(entityInstance, propertyMapping);

        QString valueStr =
            insertParameter(boundParameters, propertyMapping.tableFieldName(), propertyValue);

        fieldsList.push_back(escapeIdentifier(propertyMapping.tableFieldName()));
        valuesList.push_back(valueStr);
    }

    QString fieldsStr = fieldsList.join(',');
    QString valuesStr = valuesList.join(',');

    QString statement = QStringLiteral("INSERT INTO %1(%2) VALUES(%3)")
                            .arg(escapeIdentifier(relation.tableName()), fieldsStr, valuesStr);

    return statement;
}

QString QOrmSqliteStatementGenerator::generateInsertIntoStatement(
    const QString& destinationTableName,
    const QStringList& destinationColumns,
    const QString& sourceTableName,
    const QStringList& sourceColumns)
{
    QStringList columnList;

    for (const QString& destinationColumn : destinationColumns)
    {
        if (sourceColumns.contains(destinationColumn))
        {
            columnList += QString{R"("%1")"}.arg(destinationColumn);
        }
    }

    return QString(R"(INSERT INTO "%1"(%2) SELECT %2 FROM "%3")")
        .arg(destinationTableName, columnList.join(','), sourceTableName);
}

QString QOrmSqliteStatementGenerator::generateUpdateStatement(const QOrmMetadata& relation,
                                                              const QObject* entityInstance,
                                                              QVariantMap& boundParameters)
{
    if (relation.objectIdMapping() == nullptr)
        qFatal("QtOrm: Unable to update entity without object ID property");

    QStringList setList;

    for (const QOrmPropertyMapping& propertyMapping : relation.propertyMappings())
    {
        if (propertyMapping.isTransient() || propertyMapping.isObjectId())
            continue;

        QVariant propertyValue = propertyValueForQuery(entityInstance, propertyMapping);

        QString parameterName =
            insertParameter(boundParameters, propertyMapping.tableFieldName(), propertyValue);
        setList.push_back(QString{"%1 = %2"}.arg(propertyMapping.tableFieldName(), parameterName));
    }

    QVariant objectId = QOrmPrivate::objectIdPropertyValue(entityInstance, relation);

    QString whereClause =
        generateWhereClause(QOrmFilter{*relation.objectIdMapping() == objectId}, boundParameters);

    QStringList parts = {"UPDATE", escapeIdentifier(relation.tableName()), "SET", setList.join(','), whereClause};

    return parts.join(QChar(' '));
}

QString QOrmSqliteStatementGenerator::generateSelectStatement(const QOrmQuery& query,
                                                              QVariantMap& boundParameters)
{
    Q_ASSERT(query.operation() == QOrm::Operation::Read ||
             query.operation() == QOrm::Operation::Count);

    QString projection = query.operation() == QOrm::Operation::Read
                             ? QString{"*"}
                             : QString{"COUNT(*) AS %1"}.arg(escapeIdentifier("count"));
    ;

    QStringList parts = {"SELECT",
                         projection,
                         generateFromClause(query.relation(), boundParameters)};

    if (query.expressionFilter().has_value())
        parts += generateWhereClause(*query.expressionFilter(), boundParameters);

    parts += generateOrderClause(query.order());

    parts += generateLimitOffsetClause(query.limit(), query.offset(), boundParameters);

    return parts.join(QChar{' '});
}

QString QOrmSqliteStatementGenerator::generateDeleteStatement(const QOrmMetadata& relation,
                                                              const QOrmFilter& filter,
                                                              QVariantMap& boundParameters)
{
    QStringList parts = {"DELETE",
                         generateFromClause(QOrmRelation{relation}, boundParameters),
                         generateWhereClause(filter, boundParameters)};

    if (m_options.testFlag(WithReturningClause))
    {
        parts.push_back(generateReturningIdClause(relation));
    }

    return parts.join(QChar{' '});
}

QString QOrmSqliteStatementGenerator::generateDeleteStatement(const QOrmMetadata& relation,
                                                              const QObject* instance,
                                                              QVariantMap& boundParameters)
{
    Q_ASSERT(relation.objectIdMapping() != nullptr);

    QVariant objectId = QOrmPrivate::objectIdPropertyValue(instance, relation);

    return generateDeleteStatement(relation,
                                   QOrmFilter{*relation.objectIdMapping() == objectId},
                                   boundParameters);
}

QString QOrmSqliteStatementGenerator::generateFromClause(const QOrmRelation& relation,
                                                         QVariantMap& boundParameters)
{
    switch (relation.type())
    {
        case QOrm::RelationType::Mapping:
            return QString{"FROM %1"}.arg(escapeIdentifier(relation.mapping()->tableName()));

        case QOrm::RelationType::Query:
            Q_ASSERT(relation.query()->operation() == QOrm::Operation::Read);

            return QString{"FROM (%1)"}.arg(generate(*relation.query(), boundParameters));
    }

    Q_ORM_UNEXPECTED_STATE;
}

QString QOrmSqliteStatementGenerator::generateWhereClause(const QOrmFilter& filter,
                                                          QVariantMap& boundParameters)
{
    QString whereClause;

    if (filter.type() == QOrm::FilterType::Expression)
    {
        Q_ASSERT(filter.expression() != nullptr);

        whereClause = generateCondition(*filter.expression(), boundParameters);

        if (!whereClause.isEmpty())
            whereClause = "WHERE " + whereClause;
    }

    return whereClause;
}

QString QOrmSqliteStatementGenerator::generateOrderClause(const std::vector<QOrmOrder>& order)
{
    QStringList parts;

    for (const QOrmOrder& element : order)
    {
        parts += element.mapping().tableFieldName() % (element.direction() == Qt::AscendingOrder
                                                           ? QStringLiteral(" ASC")
                                                           : QStringLiteral(" DESC"));
    }

    return parts.empty() ? QString{} : QStringLiteral("ORDER BY ") % parts.join(',');
}

// For SQLite >= 3.35.0
// https://www.sqlite.org/lang_returning.html
QString QOrmSqliteStatementGenerator::generateReturningIdClause(const QOrmMetadata& relation)
{
    return QString{R"(RETURNING %1.%2 AS %3)"}
        .arg(escapeIdentifier(relation.tableName()))
        .arg(escapeIdentifier(relation.objectIdMapping()->tableFieldName()))
        .arg(escapeIdentifier(relation.objectIdMapping()->classPropertyName()));
}

QString QOrmSqliteStatementGenerator::generateCondition(const QOrmFilterExpression& expression,
                                                        QVariantMap& boundParameters)
{
    switch (expression.type())
    {
        case QOrm::FilterExpressionType::TerminalPredicate:
            Q_ASSERT(expression.terminalPredicate() != nullptr);
            return generateCondition(*expression.terminalPredicate(), boundParameters);

        case QOrm::FilterExpressionType::BinaryPredicate:
            Q_ASSERT(expression.binaryPredicate() != nullptr);
            return generateCondition(*expression.binaryPredicate(), boundParameters);

        case QOrm::FilterExpressionType::UnaryPredicate:
            Q_ASSERT(expression.unaryPredicate() != nullptr);
            return generateCondition(*expression.unaryPredicate(), boundParameters);
    }

    Q_ORM_UNEXPECTED_STATE;
}

QString QOrmSqliteStatementGenerator::generateCondition(
    const QOrmFilterTerminalPredicate& predicate,
    QVariantMap& boundParameters)
{
    Q_ASSERT(predicate.isResolved());

    QVariant value;
    QString statement;

    if (predicate.propertyMapping()->isReference() && !predicate.value().isNull())
    {
        const QOrmMetadata* referencedEntity = predicate.propertyMapping()->referencedEntity();
        Q_ASSERT(referencedEntity != nullptr);

        auto referencedInstance = predicate.value().value<QObject*>();

        if (referencedInstance != nullptr)
        {
            value = QOrmPrivate::objectIdPropertyValue(referencedInstance, *referencedEntity);
        }
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        else if (predicate.value().type() ==
                 static_cast<QVariant::Type>(referencedEntity->objectIdMapping()->dataType()))
#else
        else if (predicate.value().typeId() == referencedEntity->objectIdMapping()->dataType())
#endif
        {
            value = predicate.value();
        }
        else
        {
            qCCritical(qtorm).nospace().noquote()
                << "Unexpected filter value type encountered (filter property: "
                << predicate.propertyMapping()->enclosingEntity().className()
                << "::" << predicate.propertyMapping()->classPropertyName()
                << ", filter value: " << predicate.value()
                << "). The filter value must be either an instance of "
                << referencedEntity->className() << ", a nullptr, or a value of "
                << referencedEntity->className()
                << "::" << referencedEntity->objectIdMapping()->classPropertyName() << " of type "
                << referencedEntity->objectIdMapping()->dataTypeName();
            Q_ORM_UNEXPECTED_STATE;
        }
    }
    else
    {
        value = predicate.value();
    }

    if (value.isNull())
    {
        static const QHash<QOrm::Comparison, QString> comparisonOps = {
            {QOrm::Comparison::Equal, "IS NULL"}, {QOrm::Comparison::NotEqual, "IS NOT NULL"}};

        if (!comparisonOps.contains(predicate.comparison()))
        {
            qCCritical(qtorm) << predicate.propertyMapping()->tableFieldName()
                              << "is compared to null using operator" << predicate.comparison()
                              << ". Only = and != are supported when comparing to null.";
            qFatal("qtorm: Unexpected query.");
        }

        statement =
            QString{"%1 %2"}.arg(escapeIdentifier(predicate.propertyMapping()->tableFieldName()),
                                 comparisonOps[predicate.comparison()]);
    }
    else
    {
        static const QHash<QOrm::Comparison, QString> comparisonOps = {
            {QOrm::Comparison::Less, "<"},
            {QOrm::Comparison::Equal, "="},
            {QOrm::Comparison::Greater, ">"},
            {QOrm::Comparison::NotEqual, "<>"},
            {QOrm::Comparison::LessOrEqual, "<="},
            {QOrm::Comparison::GreaterOrEqual, ">="},
            {QOrm::Comparison::InList, "IN"},
            {QOrm::Comparison::NotInList, "NOT IN"},
            {QOrm::Comparison::Contains, "LIKE"},
            {QOrm::Comparison::NotContains, "NOT LIKE"}};

        Q_ASSERT(comparisonOps.contains(predicate.comparison()));

        if (predicate.comparison() == QOrm::Comparison::InList ||
            predicate.comparison() == QOrm::Comparison::NotInList)
        {
            QStringList parameterKeys;

            QVariantList list = value.toList();

            for (int i = 0; i < list.size(); ++i)
            {
                QString parameterKey =
                    QString{"%1_%2"}.arg(predicate.propertyMapping()->tableFieldName()).arg(i);
                parameterKey = insertParameter(boundParameters, parameterKey, list.at(i));
                parameterKeys.push_back(parameterKey);
            }

            statement =
                QString{"%1 %2 (%3)"}.arg(escapeIdentifier(
                                              predicate.propertyMapping()->tableFieldName()),
                                          comparisonOps[predicate.comparison()],
                                          parameterKeys.join(", "));
        }
        else if (predicate.comparison() == QOrm::Comparison::Contains ||
                 predicate.comparison() == QOrm::Comparison::NotContains)
        {
            QString pattern = '%' % value.toString() % '%';

            QString parameterKey = insertParameter(boundParameters,
                                                   predicate.propertyMapping()->tableFieldName(),
                                                   pattern);
            statement = QString{"%1 %2 %3"}.arg(escapeIdentifier(
                                                    predicate.propertyMapping()->tableFieldName()),
                                                comparisonOps[predicate.comparison()],
                                                parameterKey);
        }
        else
        {
            QString parameterKey = insertParameter(boundParameters,
                                                   predicate.propertyMapping()->tableFieldName(),
                                                   value);

            statement = QString{"%1 %2 %3"}.arg(escapeIdentifier(
                                                    predicate.propertyMapping()->tableFieldName()),
                                                comparisonOps[predicate.comparison()],
                                                parameterKey);
        }
    }

    return statement;
}

QString QOrmSqliteStatementGenerator::generateCondition(const QOrmFilterBinaryPredicate& predicate,
                                                        QVariantMap& boundParameters)
{
    QString lhsExpr = generateCondition(predicate.lhs(), boundParameters);
    QString rhsExpr = generateCondition(predicate.rhs(), boundParameters);

    QString op;

    switch (predicate.logicalOperator())
    {
        case QOrm::BinaryLogicalOperator::Or:
            op = "OR";
            break;
        case QOrm::BinaryLogicalOperator::And:
            op = "AND";
            break;
    }

    Q_ASSERT(!op.isEmpty());

    return QString{"(%1) %2 (%3)"}.arg(lhsExpr, op, rhsExpr);
}

QString QOrmSqliteStatementGenerator::generateCondition(const QOrmFilterUnaryPredicate& predicate,
                                                        QVariantMap& boundParameters)
{
    QString rhsExpr = generateCondition(predicate.rhs(), boundParameters);
    Q_ASSERT(predicate.logicalOperator() == QOrm::UnaryLogicalOperator::Not);

    return QString{"NOT (%1)"}.arg(rhsExpr);
}

QString QOrmSqliteStatementGenerator::generateCreateTableStatement(
    const QOrmMetadata& entity,
    std::optional<QString> overrideTableName)
{
    QStringList fields;
    QMap<QString, QStringList> uniqueGroups;

    for (const QOrmPropertyMapping& mapping : entity.propertyMappings())
    {
        if (mapping.isTransient())
            continue;

        QStringList columnDefs;

        if (mapping.isReference())
        {
            Q_ASSERT(mapping.referencedEntity()->objectIdMapping() != nullptr);

            columnDefs += {escapeIdentifier(mapping.tableFieldName()),
                           toSqliteType(mapping.referencedEntity()->objectIdMapping()->dataType())};

            if (mapping.hasForeignKey())
                columnDefs += {QStringLiteral("REFERENCES"),
                              escapeIdentifier(mapping.referencedEntity()->tableName()),
                              QStringLiteral("("),
                              escapeIdentifier(mapping.referencedEntity()->objectIdMapping()->tableFieldName()),
                              QStringLiteral(")")};
        }
        else
        {
            columnDefs +=
                {escapeIdentifier(mapping.tableFieldName()), toSqliteType(mapping.dataType())};

            if (mapping.isObjectId())
                columnDefs.push_back(QStringLiteral("PRIMARY KEY"));

            if (mapping.isAutogenerated())
                columnDefs.push_back(QStringLiteral("AUTOINCREMENT"));
        }

        if (mapping.isNotNull())
            columnDefs.push_back(QStringLiteral("NOT NULL"));

        if (mapping.isUnique())
        {
            if (mapping.uniqueGroup().isEmpty())
            {
                columnDefs.push_back(QStringLiteral("UNIQUE"));
            }
            else
            {
                uniqueGroups[mapping.uniqueGroup()].push_back(escapeIdentifier(mapping.tableFieldName()));
            }
        }

        fields.push_back(columnDefs.join(' '));
    }

    for (const QStringList& columns : std::as_const(uniqueGroups)) {
        QStringList uniqueDef = {QStringLiteral("UNIQUE("),
                                columns.join(','),
                                QStringLiteral(")")};
        fields.push_back(uniqueDef.join(' '));
    }

    QString fieldsStr = fields.join(',');

    Q_ASSERT(!overrideTableName.has_value() || !overrideTableName->isEmpty());
    QString effectiveTableName{overrideTableName.value_or(entity.tableName())};

    return QStringLiteral("CREATE TABLE %1(%2)").arg(escapeIdentifier(effectiveTableName), fieldsStr);
}

QString QOrmSqliteStatementGenerator::generateAlterTableAddColumnStatement(
    const QOrmMetadata& relation,
    const QOrmPropertyMapping& propertyMapping)
{
    Q_ASSERT(!propertyMapping.isTransient());

    QString dataType;

    if (propertyMapping.isReference())
    {
        Q_ASSERT(propertyMapping.referencedEntity()->objectIdMapping() != nullptr);
        dataType = toSqliteType(propertyMapping.referencedEntity()->objectIdMapping()->dataType());
    }
    else
    {
        dataType = toSqliteType(propertyMapping.dataType());
    }

    QString possibleFk = "";

    if (propertyMapping.isReference() && propertyMapping.hasForeignKey())
    {
        QStringList fkDef = {QStringLiteral("REFERENCES"),
                            escapeIdentifier(propertyMapping.referencedEntity()->tableName()),
                            QStringLiteral("("),
                            escapeIdentifier(propertyMapping.referencedEntity()->objectIdMapping()->tableFieldName()),
                            QStringLiteral(")")};
        possibleFk = fkDef.join(' ');
    }

    return QStringLiteral("ALTER TABLE %1 ADD COLUMN %2 %3 %4")
        .arg(escapeIdentifier(relation.tableName()),
             escapeIdentifier(propertyMapping.tableFieldName()),
             dataType,
             possibleFk);
}

QString QOrmSqliteStatementGenerator::generateDropTableStatement(const QOrmMetadata& entity)
{
    return QStringLiteral("DROP TABLE %1").arg(escapeIdentifier(entity.tableName()));
}

QString QOrmSqliteStatementGenerator::generateRenameTableStatement(const QString& oldName,
                                                                   const QString& newName)
{
    return QStringLiteral("ALTER TABLE %1 RENAME TO %2")
        .arg(escapeIdentifier(oldName), escapeIdentifier(newName));
}

QString QOrmSqliteStatementGenerator::generateLimitOffsetClause(std::optional<int> limit,
                                                                std::optional<int> offset,
                                                                QVariantMap& boundParameters)
{
    QStringList parts;

    if (offset.has_value())
    {
        QString limitKey = insertParameter(boundParameters, "limit", limit.value_or(-1));
        QString offsetKey = insertParameter(boundParameters, "offset", offset.value());
        return QString{"LIMIT %1 OFFSET %2"}.arg(limitKey, offsetKey);
    }

    if (limit.has_value())
    {
        QString limitKey = insertParameter(boundParameters, "limit", limit.value());
        return QString{"LIMIT %1"}.arg(limitKey);
    }

    return {};
}

QString QOrmSqliteStatementGenerator::toSqliteType(QMetaType::Type type)
{
    // SQLite data types: https://sqlite.org/datatype3.html
    switch (type)
    {
        case QMetaType::Int:
        case QMetaType::UInt:
        case QMetaType::Long:
        case QMetaType::LongLong:
        case QMetaType::ULongLong:
            return QStringLiteral("INTEGER");

        case QMetaType::Double:
            return QStringLiteral("REAL");

        case QMetaType::Bool:
        case QMetaType::QDate:
        case QMetaType::QTime:
        case QMetaType::QDateTime:
            return QStringLiteral("NUMERIC");

        case QMetaType::Char:
        case QMetaType::QString:
        case QMetaType::QVariant:
            return QStringLiteral("TEXT");

        default:
            return QStringLiteral("BLOB");
    }
}

QString QOrmSqliteStatementGenerator::escapeIdentifier(const QString& identifier)
{
    return identifier.startsWith('"') && identifier.endsWith('"')
               ? identifier
               : QString{R"("%1")"}.arg(identifier);
}

QT_END_NAMESPACE
