#ifndef ERDEFFECTIVECHANGEMERGER_H
#define ERDEFFECTIVECHANGEMERGER_H

#include "erdeffectivechange.h"
#include "common/global.h"
#include "db/db.h"
#include <QStringList>
#include <QUuid>
#include <QScopeGuard>

class ErdChange;

/**
 * @brief The ErdEffectiveChangeMerger class is responsible for merging multiple ErdChange instances
 *        into a minimized list of ErdEffectiveChange instances.
 */
class ErdEffectiveChangeMerger
{
    public:
        /**
         * @brief Constructor
         * @param schemaBase Database schema as list of DDL statements in state applicable for changes to be merged.
         * @param dbName Name of the database (used for in-memory database creation, so that all extensions/functions, etc are loaded by name).
         */
        ErdEffectiveChangeMerger(const QStringList& schemaBase, const QString& dbName);

        /**
         * @brief Merges a list of ErdChange instances into a minimized list of ErdEffectiveChange instances.
         * @param changes List of ErdChange instances to be merged.
         * @return List of merged ErdEffectiveChange instances.
         */
        QList<ErdEffectiveChange> merge(const QList<ErdChange*>& changes);

        /**
         * @brief Generates DDL statements for the provided effective change.
         * @param change Effective change to generate DDL for.
         * @return List of DDL statements representing the effective change.
         *
         * This method uses an internal cache to avoid regenerating DDL for the same change multiple times,
         * unlike the overload that accepts a Db* parameter.
         *
         * Use this variant of the method only on an ErdEffectiveChange that was generated by this merger instance.
         * Otherwise it will return empty list.
         *
         * @see generateDdl(const ErdEffectiveChange& change, Db* db)
         */
        QStringList getDdlForChange(const ErdEffectiveChange& change) const;

        /**
         * @brief Creates an in-memory database and applies the provided schema DDL statements to it.
         * @param schemaDdls Database schema as list of DDL statements in state applicable for changes to be merged.
         * @param name Name of the database (used for in-memory database creation, so that all extensions/functions, etc are loaded by name).
         * @return Pointer to created in-memory database, or nullptr on failure.
         */
        static Db* createMemDbWithSchema(const QStringList& schemaDdls, const QString& name);

        /**
         * @brief Reads the full database schema as a list of DDL statements.
         * @param db Database to read the schema from.
         * @return List of DDL statements representing the database schema.
         *
         * This method retrieves the schema for tables, views, triggers, and indexes, in that particular order,
         * so they can be reused in returned order to recreate the database schema accurately.
         */
        static QStringList readDbSchema(Db* db);

        /**
         * @brief Flattens a list of ErdChange instances into a single-level list.
         * @param changes List of ErdChange instances to be flattened.
         * @return Flattened list of ErdChange instances.
         *
         * This method processes each ErdChange in the input list. If an ErdChange is a composite change
         * (i.e., it contains multiple sub-changes), the method extracts its sub-changes and adds them
         * individually to the output list. Non-composite changes are added directly to the output list.
         */
        static QList<ErdChange*> flatten(const QList<ErdChange*>& changes);

    private:
        struct TableModifierAftermath
        {
            QStringList modifiedTables;
            QStringList modifiedViews;
        };

        ErdEffectiveChange merge(const QList<ErdEffectiveChange>& theList, int& idx, Db* referenceDb, Db* workingDb);
        ErdEffectiveChange mergeToCreateByStrategy(const QList<ErdEffectiveChange>& theList, int& idx, Db* referenceDb, Db* workingDb);
        ErdEffectiveChange mergeToDropByStrategy(const QList<ErdEffectiveChange>& theList, int& idx, Db* referenceDb, Db* workingDb);
        ErdEffectiveChange mergeToModifyByStrategy(const QList<ErdEffectiveChange>& theList, int& idx, Db* referenceDb, Db* workingDb);
        ErdEffectiveChange strategyMultipleModifyToOne(const QList<ErdEffectiveChange>& theList, int& idx, Db* referenceDb, Db* workingDb);
        ErdEffectiveChange strategyModifyToDropAhead(const QList<ErdEffectiveChange>& theList, int& idx, Db* referenceDb, Db* workingDb);
        ErdEffectiveChange strategyMultipleModifyToCreate(const QList<ErdEffectiveChange>& theList, int& idx, Db* referenceDb, Db* workingDb);
        ErdEffectiveChange strategyDropToCreateChange(const QList<ErdEffectiveChange>& theList, int& idx, Db* referenceDb, Db* workingDb);
        ErdEffectiveChange strategyMultipleDropToRaw(const QList<ErdEffectiveChange>& theList, int& idx, Db* referenceDb, Db* workingDb);

        /**
         * @brief Generates a schema snapshot of the provided database as a single string.
         * @param db Database to generate the schema snapshot from.
         * @return String representing the database schema.
         */
        QString schemaSnapshot(Db* db);

        /**
         * @brief Tests whether applying the merged change to the working database
         *        results in the same schema as applying the reference changes to the reference database.
         * @param mergedChange Planned merged change to be tested
         * @param referenceChanges List of reference changes to be applied to the reference database.
         * @param referenceDb In-memory db with schema before applying reference changes.
         * @param workingDb In-memory db with schema before applying merged change.
         *        It must have same schema as referenceDb before applying changes, but it has to be a separate in-memory db.
         * @return True if the merged change produces the same schema as the reference changes, false otherwise.
         *
         * Any tested changes are rolled back after the test, so both databases remain unchanged.
         */
        bool testAgainstOriginal(ErdEffectiveChange mergedChange, const QList<ErdEffectiveChange>& referenceChanges,
                                 Db* referenceDb, Db* workingDb);

        /**
         * @brief Outputs debug information about differences between two schema snapshots.
         * @param refSchema Schema from reference database.
         * @param workingSchema Schema from working database.
         *
         * It's used in testAgainstOriginal() method to log differences when schemas do not match.
         */
        void debugSnapshotDiff(const QString& refSchema, const QString& workingSchema);

        /**
         * @brief Executes a single effective change on both reference and working databases.
         * @param change Effective change to be executed.
         * @param referenceDb In-memory db with schema before applying reference changes.
         * @param workingDb In-memory db with schema before applying merged change.
         *
         * This method applies the same change to both databases to keep them in sync.
         * It is executed after the effective change is accepted as valid in the merging process.
         */
        void executeOneChangeOnBothDbs(ErdEffectiveChange change, Db* referenceDb, Db* workingDb);

        /**
         * @brief Executes the provided effective change on the specified database.
         * @param change Effective change to be executed.
         * @param db Database to execute the change on.
         *
         * This method applies the effective change to the given database. No transactions are involved,
         * so the caller is responsible for managing transactions if needed.
         */
        void executeOnDb(ErdEffectiveChange change, Db* db);

        /**
         * @brief Generates DDL statements for the provided effective change using the specified database.
         * @param change Effective change to generate DDL for.
         * @param db Database to use for generating the DDL statements.
         * @return List of DDL statements representing the effective change.
         */
        QStringList generateDdl(const ErdEffectiveChange& change, Db* db, TableModifierAftermath& aftermath);

        /**
         * @brief Gets DDL statements for the provided effective change using the specified database,
         *        with caching to avoid redundant generation.
         * @param change Effective change to get DDL for.
         * @param db Database to use for generating the DDL statements.
         * @return List of DDL statements representing the effective change.
         */
        QStringList getDdlForChange(const ErdEffectiveChange& change, Db* db);

        /**
         * @brief Creates a scope guard that rolls back to a savepoint in both databases upon scope exit.
         * @param referenceDb In-memory db with schema for applying reference changes.
         * @param workingDb In-memory db with schema for applying merged changes.
         * @return Scope guard that rolls back to the savepoint in both databases when it goes out of scope.
         *
         * This is to be used when testing changes on both databases, so that any changes made during the test
         * are rolled back automatically when the scope is exited.
         */
        auto scopedTxRollback(Db* referenceDb, Db* workingDb)
        {
            static_qstring(savepointTpl, "SAVEPOINT '%1'");
            static_qstring(rollbackToTpl, "ROLLBACK TO '%1'");

            QString testSavepoint = QUuid::createUuid().toString(QUuid::WithoutBraces);
            referenceDb->exec(savepointTpl.arg(testSavepoint));
            workingDb->exec(savepointTpl.arg(testSavepoint));

            return qScopeGuard([referenceDb, workingDb, testSavepoint]()
            {
                referenceDb->exec(rollbackToTpl.arg(testSavepoint));
                workingDb->exec(rollbackToTpl.arg(testSavepoint));
            });
        }

        QStringList schemaBase;
        QString dbName;
        QHash<QString, QStringList> ddlCacheByChangeId;
        QHash<QString, TableModifierAftermath> aftermathByChangeId;
};

#endif // ERDEFFECTIVECHANGEMERGER_H
