"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module iModels
 */
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
    if (value !== null && value !== void 0) {
        if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
        var dispose, inner;
        if (async) {
            if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
            dispose = value[Symbol.asyncDispose];
        }
        if (dispose === void 0) {
            if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
            dispose = value[Symbol.dispose];
            if (async) inner = dispose;
        }
        if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
        if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
        env.stack.push({ value: value, dispose: dispose, async: async });
    }
    else if (async) {
        env.stack.push({ async: true });
    }
    return value;
};
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
    return function (env) {
        function fail(e) {
            env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
            env.hasError = true;
        }
        var r, s = 0;
        function next() {
            while (r = env.stack.pop()) {
                try {
                    if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
                    if (r.dispose) {
                        var result = r.dispose.call(r.value);
                        if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
                    }
                    else s |= 1;
                }
                catch (e) {
                    fail(e);
                }
            }
            if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
            if (env.hasError) throw env.error;
        }
        return next();
    };
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
    var e = new Error(message);
    return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
});
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChangeSummaryManager = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const path = require("path");
const BackendLoggerCategory_1 = require("./BackendLoggerCategory");
const BriefcaseManager_1 = require("./BriefcaseManager");
const ECDb_1 = require("./ECDb");
const IModelDb_1 = require("./IModelDb");
const IModelHost_1 = require("./IModelHost");
const IModelJsFs_1 = require("./IModelJsFs");
const Symbols_1 = require("./internal/Symbols");
const loggerCategory = BackendLoggerCategory_1.BackendLoggerCategory.ECDb;
/** Class to extract Change Summaries for a briefcase.
 *
 * See also:
 * - [ChangeSummary Overview]($docs/learning/ChangeSummaries)
 * @beta
 */
class ChangeSummaryManager {
    static _currentIModelChangeSchemaVersion = { read: 2, write: 0, minor: 0 };
    /** Determines whether the *Change Cache file* is attached to the specified iModel or not
     * @param iModel iModel to check whether a *Change Cache file* is attached
     * @returns Returns true if the *Change Cache file* is attached to the iModel. false otherwise
     */
    static isChangeCacheAttached(iModel) {
        if (!iModel || !iModel.isOpen)
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Briefcase must be open");
        return iModel[Symbols_1._nativeDb].isChangeCacheAttached();
    }
    /** Attaches the *Change Cache file* to the specified iModel if it hasn't been attached yet.
     * A new *Change Cache file* will be created for the iModel if it hasn't existed before.
     * @param iModel iModel to attach the *Change Cache file* file to
     * @throws [IModelError]($common)
     */
    static attachChangeCache(iModel) {
        if (!iModel || !iModel.isOpen)
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Briefcase must be open");
        if (ChangeSummaryManager.isChangeCacheAttached(iModel))
            return;
        const changesCacheFilePath = BriefcaseManager_1.BriefcaseManager.getChangeCachePathName(iModel.iModelId);
        if (!IModelJsFs_1.IModelJsFs.existsSync(changesCacheFilePath)) {
            const env_1 = { stack: [], error: void 0, hasError: false };
            try {
                const changeCacheFile = __addDisposableResource(env_1, new ECDb_1.ECDb(), false);
                ChangeSummaryManager.createChangeCacheFile(iModel, changeCacheFile, changesCacheFilePath);
            }
            catch (e_1) {
                env_1.error = e_1;
                env_1.hasError = true;
            }
            finally {
                __disposeResources(env_1);
            }
        }
        (0, core_bentley_1.assert)(IModelJsFs_1.IModelJsFs.existsSync(changesCacheFilePath));
        const res = iModel[Symbols_1._nativeDb].attachChangeCache(changesCacheFilePath);
        if (res !== core_bentley_1.DbResult.BE_SQLITE_OK)
            throw new core_common_1.IModelError(res, `Failed to attach Change Cache file to ${iModel.pathName}.`);
    }
    /** Detaches the *Change Cache file* from the specified iModel.
     * - note that this method will cause any pending (currently running or queued) queries to fail
     * @param iModel iModel to detach the *Change Cache file* to
     * @throws [IModelError]($common) in case of errors, e.g. if no *Change Cache file* was attached before.
     */
    static detachChangeCache(iModel) {
        if (!iModel || !iModel.isOpen)
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Briefcase must be open");
        iModel.clearCaches();
        const res = iModel[Symbols_1._nativeDb].detachChangeCache();
        if (res !== core_bentley_1.DbResult.BE_SQLITE_OK)
            throw new core_common_1.IModelError(res, `Failed to detach Change Cache file from ${iModel.pathName}.`);
    }
    static openOrCreateChangesFile(iModel) {
        if (!iModel?.isOpen)
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadArg, "Invalid iModel handle. iModel must be open.");
        const changesFile = new ECDb_1.ECDb();
        const changeCacheFilePath = BriefcaseManager_1.BriefcaseManager.getChangeCachePathName(iModel.iModelId);
        if (IModelJsFs_1.IModelJsFs.existsSync(changeCacheFilePath)) {
            ChangeSummaryManager.openChangeCacheFile(changesFile, changeCacheFilePath);
            return changesFile;
        }
        try {
            ChangeSummaryManager.createChangeCacheFile(iModel, changesFile, changeCacheFilePath);
            return changesFile;
        }
        catch (e) {
            // delete cache file again in case it was created but schema import failed
            if (IModelJsFs_1.IModelJsFs.existsSync(changeCacheFilePath))
                IModelJsFs_1.IModelJsFs.removeSync(changeCacheFilePath);
            throw e;
        }
    }
    static createChangeCacheFile(iModel, changesFile, changeCacheFilePath) {
        if (!iModel?.isOpen)
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadArg, "Invalid iModel object. iModel must be open.");
        const stat = iModel[Symbols_1._nativeDb].createChangeCache(changesFile[Symbols_1._nativeDb], changeCacheFilePath);
        if (stat !== core_bentley_1.DbResult.BE_SQLITE_OK)
            throw new core_common_1.IModelError(stat, `Failed to create Change Cache file at "${changeCacheFilePath}".`);
        // Extended information like changeset ids, push dates are persisted in the IModelChange ECSchema
        changesFile.importSchema(ChangeSummaryManager.getExtendedSchemaPath());
    }
    static openChangeCacheFile(changesFile, changeCacheFilePath) {
        changesFile.openDb(changeCacheFilePath, ECDb_1.ECDbOpenMode.FileUpgrade);
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        const actualSchemaVersion = changesFile.withPreparedStatement("SELECT VersionMajor read,VersionWrite write,VersionMinor minor FROM meta.ECSchemaDef WHERE Name='IModelChange'", (stmt) => {
            if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
                throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, "File is not a valid Change Cache file.");
            return stmt.getRow();
        });
        if (actualSchemaVersion.read === ChangeSummaryManager._currentIModelChangeSchemaVersion.read &&
            actualSchemaVersion.write === ChangeSummaryManager._currentIModelChangeSchemaVersion.write &&
            actualSchemaVersion.minor === ChangeSummaryManager._currentIModelChangeSchemaVersion.minor)
            return;
        changesFile.importSchema(ChangeSummaryManager.getExtendedSchemaPath());
    }
    static getExtendedSchemaPath() { return path.join(IModelHost_1.KnownLocations.packageAssetsDir, "IModelChange.02.00.00.ecschema.xml"); }
    static isSummaryAlreadyExtracted(changesFile, changeSetId) {
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        return changesFile.withPreparedStatement("SELECT Summary.Id summaryid FROM imodelchange.ChangeSet WHERE WsgId=?", (stmt) => {
            stmt.bindString(1, changeSetId);
            if (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step())
                return stmt.getValue(0).getId();
            return undefined;
        });
    }
    static addExtendedInfos(changesFile, changeSummaryId, changesetWsgId, changesetParentWsgId, description, changesetPushDate, changeSetUserCreated) {
        changesFile.withCachedWriteStatement("INSERT INTO imodelchange.ChangeSet(Summary.Id,WsgId,ParentWsgId,Description,PushDate,UserCreated) VALUES(?,?,?,?,?,?)", (stmt) => {
            stmt.bindId(1, changeSummaryId);
            stmt.bindString(2, changesetWsgId);
            if (changesetParentWsgId)
                stmt.bindString(3, changesetParentWsgId);
            if (description)
                stmt.bindString(4, description);
            if (changesetPushDate)
                stmt.bindDateTime(5, changesetPushDate);
            if (changeSetUserCreated)
                stmt.bindString(6, changeSetUserCreated);
            const r = stmt.stepForInsert();
            if (r.status !== core_bentley_1.DbResult.BE_SQLITE_DONE)
                throw new core_common_1.IModelError(r.status, `Failed to add changeset information to extracted change summary ${changeSummaryId}`);
        });
    }
    /** Queries the ChangeSummary for the specified change summary id
     *
     * See also
     * - `ECDbChange.ChangeSummary` ECClass in the *ECDbChange* ECSchema
     * - [Change Summary Overview]($docs/learning/ChangeSummaries)
     * @param iModel iModel
     * @param changeSummaryId ECInstanceId of the ChangeSummary
     * @returns Returns the requested ChangeSummary object
     * @throws [IModelError]($common) If change summary does not exist for the specified id, or if the
     * change cache file hasn't been attached, or in case of other errors.
     */
    static queryChangeSummary(iModel, changeSummaryId) {
        if (!ChangeSummaryManager.isChangeCacheAttached(iModel))
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadArg, "Change Cache file must be attached to iModel.");
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        return iModel.withPreparedStatement("SELECT WsgId,ParentWsgId,Description,PushDate,UserCreated FROM ecchange.imodelchange.ChangeSet WHERE Summary.Id=?", (stmt) => {
            stmt.bindId(1, changeSummaryId);
            if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
                throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadArg, `No ChangeSet information found for ChangeSummary ${changeSummaryId}.`);
            const row = stmt.getRow();
            return { id: changeSummaryId, changeSet: { wsgId: row.wsgId, parentWsgId: row.parentWsgId, description: row.description, pushDate: row.pushDate, userCreated: row.userCreated } };
        });
    }
    /** Queries the InstanceChange for the specified instance change id.
     *
     * See also
     * - `ECDbChange.InstanceChange` ECClass in the *ECDbChange* ECSchema
     * - [Change Summary Overview]($docs/learning/ChangeSummaries)
     * @param iModel iModel
     * @param instanceChangeId ECInstanceId of the InstanceChange (see `ECDbChange.InstanceChange` ECClass in the *ECDbChange* ECSchema)
     * @returns Returns the requested InstanceChange object (see `ECDbChange.InstanceChange` ECClass in the *ECDbChange* ECSchema)
     * @throws [IModelError]($common) if instance change does not exist for the specified id, or if the
     * change cache file hasn't been attached, or in case of other errors.
     */
    static queryInstanceChange(iModel, instanceChangeId) {
        if (!ChangeSummaryManager.isChangeCacheAttached(iModel))
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadArg, "Change Cache file must be attached to iModel.");
        // query instance changes
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        const instanceChange = iModel.withPreparedStatement(`SELECT ic.Summary.Id summaryId, s.Name changedInstanceSchemaName, c.Name changedInstanceClassName, ic.ChangedInstance.Id changedInstanceId,
       ic.OpCode, ic.IsIndirect FROM ecchange.change.InstanceChange ic JOIN main.meta.ECClassDef c ON c.ECInstanceId = ic.ChangedInstance.ClassId
       JOIN main.meta.ECSchemaDef s ON c.Schema.Id = s.ECInstanceId WHERE ic.ECInstanceId =? `, 
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        (stmt) => {
            stmt.bindId(1, instanceChangeId);
            if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
                throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadArg, `No InstanceChange found for id ${instanceChangeId}.`);
            const row = stmt.getRow();
            const changedInstanceId = row.changedInstanceId;
            const changedInstanceClassName = `[${row.changedInstanceSchemaName}].[${row.changedInstanceClassName}]`;
            const op = row.opCode;
            return {
                id: instanceChangeId, summaryId: row.summaryId, changedInstance: { id: changedInstanceId, className: changedInstanceClassName },
                opCode: op, isIndirect: row.isIndirect,
            };
        });
        return instanceChange;
    }
    /** Retrieves the names of the properties whose values have changed for the given instance change
     *
     * See also [Change Summary Overview]($docs/learning/ChangeSummaries)
     * @param iModel iModel
     * @param instanceChangeId Id of the InstanceChange to query the properties whose values have changed
     * @returns Returns names of the properties whose values have changed for the given instance change
     * @throws [IModelError]($common) if the change cache file hasn't been attached, or in case of other errors.
     */
    static getChangedPropertyValueNames(iModel, instanceChangeId) {
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        return iModel.withPreparedStatement("SELECT AccessString FROM ecchange.change.PropertyValueChange WHERE InstanceChange.Id=?", (stmt) => {
            stmt.bindId(1, instanceChangeId);
            const selectClauseItems = [];
            while (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW) {
                // access string tokens need to be escaped as they might collide with reserved words in ECSQL or SQLite
                const accessString = stmt.getValue(0).getString();
                const accessStringTokens = accessString.split(".");
                (0, core_bentley_1.assert)(accessStringTokens.length > 0);
                let isFirstToken = true;
                let item = "";
                for (const token of accessStringTokens) {
                    if (!isFirstToken)
                        item += ".";
                    item += `[${token}]`;
                    isFirstToken = false;
                }
                selectClauseItems.push(item);
            }
            return selectClauseItems;
        });
    }
    /** Builds the ECSQL to query the property value changes for the specified instance change and the specified ChangedValueState.
     *
     * See also [Change Summary Overview]($docs/learning/ChangeSummaries)
     * @param iModel iModel
     * @param instanceChangeInfo InstanceChange to query the property value changes for
     *        changedInstance.className must be fully qualified and schema and class name must be escaped with square brackets if they collide with reserved ECSQL words: `[schema name].[class name]`
     * @param changedValueState The Changed State to query the values for. This must correspond to the [InstanceChange.OpCode]($backend) of the InstanceChange.
     * @param changedPropertyNames List of the property names for which values have changed for the specified instance change.
     *        The list can be obtained by calling [ChangeSummaryManager.getChangedPropertyValueNames]($core-backend).
     *        If omitted, the method will call the above method by itself. The parameter allows for checking first whether
     *        an instance change has any property value changes at all. If there are no property value changes, this method
     *        should not be called, as it will throw an error.
     * @returns Returns the ECSQL that will retrieve the property value changes
     * @throws [IModelError]($common) if instance change does not exist, if there are not property value changes for the instance change,
     *        if the change cache file hasn't been attached, or in case of other errors.
     */
    static buildPropertyValueChangesECSql(iModel, instanceChangeInfo, changedValueState, changedPropertyNames) {
        let selectClauseItems;
        if (!changedPropertyNames) {
            // query property value changes just to build a SELECT statement against the class of the changed instance
            selectClauseItems = ChangeSummaryManager.getChangedPropertyValueNames(iModel, instanceChangeInfo.id);
        }
        else
            selectClauseItems = changedPropertyNames;
        if (selectClauseItems.length === 0)
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadArg, `No property value changes found for InstanceChange ${instanceChangeInfo.id}.`);
        let ecsql = "SELECT ";
        selectClauseItems.map((item, index) => {
            if (index !== 0)
                ecsql += ",";
            ecsql += item;
        });
        // Avoiding parameters in the Changes function speeds up performance because ECDb can do optimizations
        // if it knows the function args at prepare time
        ecsql += ` FROM main.${instanceChangeInfo.changedInstance.className}.Changes(${instanceChangeInfo.summaryId},${changedValueState}) WHERE ECInstanceId=${instanceChangeInfo.changedInstance.id}`;
        return ecsql;
    }
    /**
     * Creates a change summary for the last applied change set to the iModel
     * @param accessToken A valid access token string
     * @param iModel iModel to extract change summaries for. The iModel must not be a standalone iModel, and must have at least one change set applied to it.
     * @returns The id of the extracted change summary.
     * @beta
     */
    static async createChangeSummary(accessToken, iModel) {
        if (!iModel?.isOpen)
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Briefcase must be open");
        const changesetId = iModel.changeset.id;
        if (!changesetId)
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "No change set was applied to the iModel");
        if (this.isChangeCacheAttached(iModel))
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Change cache must be detached before extraction");
        const iModelId = iModel.iModelId;
        const changesetsFolder = BriefcaseManager_1.BriefcaseManager.getChangeSetsPath(iModelId);
        const changeset = await IModelHost_1.IModelHost[Symbols_1._hubAccess].downloadChangeset({ accessToken: IModelHost_1.IModelHost.authorizationClient ? undefined : accessToken, iModelId, changeset: { id: iModel.changeset.id }, targetDir: changesetsFolder });
        if (!IModelJsFs_1.IModelJsFs.existsSync(changeset.pathname))
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.FileNotFound, `Failed to download change set: ${changeset.pathname}`);
        try {
            const env_2 = { stack: [], error: void 0, hasError: false };
            try {
                const changesFile = __addDisposableResource(env_2, ChangeSummaryManager.openOrCreateChangesFile(iModel), false);
                (0, core_bentley_1.assert)(changesFile[Symbols_1._nativeDb] !== undefined, "Invalid changesFile - should've caused an exception");
                let changeSummaryId = ChangeSummaryManager.isSummaryAlreadyExtracted(changesFile, changesetId);
                if (changeSummaryId !== undefined) {
                    core_bentley_1.Logger.logInfo(loggerCategory, `Change Summary for changeset already exists. It is not extracted again.`, () => ({ iModelId, changeSetId: changesetId }));
                    return changeSummaryId;
                }
                const stat = iModel[Symbols_1._nativeDb].extractChangeSummary(changesFile[Symbols_1._nativeDb], changeset.pathname);
                if (stat.error && stat.error.status !== core_bentley_1.DbResult.BE_SQLITE_OK)
                    throw new core_common_1.IModelError(stat.error.status, stat.error.message);
                (0, core_bentley_1.assert)(undefined !== stat.result);
                changeSummaryId = stat.result;
                ChangeSummaryManager.addExtendedInfos(changesFile, changeSummaryId, changesetId, changeset.parentId, changeset.description, changeset.pushDate, changeset.userCreated);
                changesFile.saveChanges();
                return changeSummaryId;
            }
            catch (e_2) {
                env_2.error = e_2;
                env_2.hasError = true;
            }
            finally {
                __disposeResources(env_2);
            }
        }
        finally {
            IModelJsFs_1.IModelJsFs.unlinkSync(changeset.pathname);
        }
    }
    /**
     * Creates change summaries for the specified iModel and a specified range of versions
     * @note This may be an expensive operation - downloads the first version and starts applying the change sets, extracting summaries one by one
     * @param args Arguments including the range of versions for which Change Summaries are to be created, and other necessary input for creation
     */
    static async createChangeSummaries(args) {
        // if we pass undefined to hubAccess methods they will use our authorizationClient to refresh the token as needed.
        const accessToken = IModelHost_1.IModelHost.authorizationClient ? undefined : args.accessToken ?? "";
        const { iModelId, iTwinId, range } = args;
        range.end = range.end ?? (await IModelHost_1.IModelHost[Symbols_1._hubAccess].getChangesetFromVersion({ accessToken, iModelId, version: core_common_1.IModelVersion.latest() })).index;
        if (range.first > range.end)
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadArg, "Invalid range of changesets");
        if (range.first === 0 && range.end === 0)
            return []; // no changesets exist, so the inclusive range is empty
        const changesets = await IModelHost_1.IModelHost[Symbols_1._hubAccess].queryChangesets({ accessToken, iModelId, range });
        // Setup a temporary briefcase to help with extracting change summaries
        const briefcasePath = BriefcaseManager_1.BriefcaseManager.getBriefcaseBasePath(iModelId);
        const fileName = path.join(briefcasePath, `ChangeSummaryBriefcase.bim`);
        if (IModelJsFs_1.IModelJsFs.existsSync(fileName))
            IModelJsFs_1.IModelJsFs.removeSync(fileName);
        let iModel;
        try {
            // Download a version that has the first change set applied
            const props = await BriefcaseManager_1.BriefcaseManager.downloadBriefcase({ accessToken, iTwinId, iModelId, asOf: { afterChangeSetId: changesets[0].id }, briefcaseId: 0, fileName });
            iModel = await IModelDb_1.BriefcaseDb.open({ fileName: props.fileName });
            const summaryIds = new Array();
            for (let index = 0; index < changesets.length; index++) {
                // Apply a change set if necessary
                if (index > 0)
                    await iModel.pullChanges({ accessToken, toIndex: changesets[index].index });
                // Create a change summary for the last change set that was applied
                const summaryId = await this.createChangeSummary(accessToken ?? await IModelHost_1.IModelHost.authorizationClient?.getAccessToken() ?? "", iModel);
                summaryIds.push(summaryId);
            }
            return summaryIds;
        }
        finally {
            if (iModel !== undefined)
                iModel.close();
            IModelJsFs_1.IModelJsFs.removeSync(fileName);
        }
    }
}
exports.ChangeSummaryManager = ChangeSummaryManager;
//# sourceMappingURL=ChangeSummaryManager.js.map