"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StashManager = exports.StashError = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const node_fs_1 = require("node:fs");
const path = require("node:path");
const BriefcaseManager_1 = require("./BriefcaseManager");
const Symbols_1 = require("./internal/Symbols");
const SQLiteDb_1 = require("./SQLiteDb");
const IModelHost_1 = require("./IModelHost");
const BackendLoggerCategory_1 = require("./BackendLoggerCategory");
const loggerCategory = BackendLoggerCategory_1.BackendLoggerCategory.StashManager;
var LockOrigin;
(function (LockOrigin) {
    LockOrigin[LockOrigin["Acquired"] = 0] = "Acquired";
    LockOrigin[LockOrigin["NewElement"] = 1] = "NewElement";
    LockOrigin[LockOrigin["Discovered"] = 2] = "Discovered";
})(LockOrigin || (LockOrigin = {}));
;
/**
 * An error originating from the StashManager API.
 * @internal
 */
var StashError;
(function (StashError) {
    StashError.scope = "itwin-StashManager";
    /** Instantiate and throw a StashError */
    function throwError(key, message) {
        core_bentley_1.ITwinError.throwError({ iTwinErrorId: { scope: StashError.scope, key }, message });
    }
    StashError.throwError = throwError;
    /** Determine whether an error object is a StashError */
    function isError(error, key) {
        return core_bentley_1.ITwinError.isError(error, StashError.scope, key);
    }
    StashError.isError = isError;
})(StashError || (exports.StashError = StashError = {}));
/**
 * Stash manager allow stash, drop, apply and merge stashes
 * @internal
 */
class StashManager {
    static STASHES_ROOT_DIR_NAME = ".stashes";
    /**
     * Retrieves the root folder path for stash files associated with the specified BriefcaseDb.
     *
     * @param db - The BriefcaseDb instance for which to determine the stash root folder.
     * @param ensureExists - If true, the stash root directory will be created if it does not already exist.
     * @returns The absolute path to the stash root directory.
     */
    static getStashRootFolder(db, ensureExists) {
        if (!db.isOpen || db.isReadonly)
            StashError.throwError("readonly", "Database is not open or is readonly");
        if (!(0, node_fs_1.existsSync)(db[Symbols_1._nativeDb].getFilePath())) {
            StashError.throwError("no-file", "Database file does not exist");
        }
        const stashDir = path.join(path.dirname(db[Symbols_1._nativeDb].getFilePath()), this.STASHES_ROOT_DIR_NAME, `${db.briefcaseId}`);
        if (ensureExists && !(0, node_fs_1.existsSync)(stashDir)) {
            (0, node_fs_1.mkdirSync)(stashDir, { recursive: true });
        }
        return stashDir;
    }
    /**
     * Retrieves the stash ID from the provided arguments.
     *
     * If the `stash` property of `args` is a string, it returns the string in lowercase.
     * If the `stash` property is an object, it returns the `id` property of the object in lowercase.
     *
     * @param args - The arguments containing the stash information, which can be either a string or an object with an `id` property.
     * @returns The stash ID as a lowercase string.
     */
    static getStashId(args) {
        return (typeof args.stash === "string" ? args.stash : args.stash.id).toLowerCase();
    }
    /**
     * Retrieves the file path to the stash file associated with the provided arguments.
     *
     * @param args - The arguments required to identify the stash, including the database reference.
     * @returns The absolute path to the stash file.
     */
    static getStashFilePath(args) {
        const stashRoot = this.getStashRootFolder(args.db, false);
        if (!(0, node_fs_1.existsSync)(stashRoot)) {
            StashError.throwError("no-stashes", "No stashes exist for this briefcase");
        }
        const stashFilePath = path.join(stashRoot, `${this.getStashId(args)}.stash`);
        if (!(0, node_fs_1.existsSync)(stashFilePath)) {
            StashError.throwError("invalid-stash", "Invalid stash");
        }
        return stashFilePath;
    }
    /**
     * Queries the stash database for lock IDs matching the specified state and origin.
     *
     * @param args - The arguments required to access the stash database.
     * @param state - The lock state to filter by.
     * @param origin - The lock origin to filter by.
     * @returns An array of lock IDs (`Id64Array`) that match the given state and origin.
     */
    static queryLocks(args, state, origin) {
        return this.withStash(args, (stashDb) => {
            const query = `SELECT JSON_GROUP_ARRAY(FORMAT('0x%x', Id)) FROM [locks] WHERE State = ${state} AND origin = ${origin}`;
            return stashDb.withPreparedSqliteStatement(query, (stmt) => {
                if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW) {
                    return JSON.parse(stmt.getValueString(0));
                }
                return [];
            });
        });
    }
    /**
     * Acquire locks for the specified stash. If this fail then stash should not be applied.
     * @param args The stash arguments.
     */
    static async acquireLocks(args) {
        const shared = this.queryLocks(args, core_common_1.LockState.Shared, LockOrigin.Acquired);
        await args.db.locks.acquireLocks({ shared });
        const exclusive = this.queryLocks(args, core_common_1.LockState.Exclusive, LockOrigin.Acquired);
        await args.db.locks.acquireLocks({ exclusive });
        const newElements = this.queryLocks(args, core_common_1.LockState.Shared, LockOrigin.NewElement);
        for (const id of newElements) {
            if (!args.db.locks.holdsExclusiveLock(id)) {
                args.db.locks[Symbols_1._elementWasCreated](id);
            }
        }
    }
    /**
     * Creates a stash of changes for the specified briefcase.
     *
     * This method generates a stash in the stash root directory for the given briefcase, using the provided description and iModelId.
     * Optionally, it can reset the briefcase by releasing all locks after stashing.
     *
     * @param args - The properties required to create a stash, including the briefcase, description, iModelId, and an optional resetBriefcase flag.
     * @returns A promise that resolves to the properties of the created stash.
     */
    static async stash(args) {
        if (!args.db.txns.hasPendingTxns) {
            StashError.throwError("nothing-to-stash", "Nothing to stash");
        }
        if (args.db.txns.hasUnsavedChanges) {
            StashError.throwError("unsaved-changes", "Unsaved changes exist");
        }
        if (args.db.txns.hasPendingSchemaChanges) {
            StashError.throwError("pending-schema-changes", "Pending schema changeset. Stashing is not currently supported for schema changes");
        }
        const stashRootDir = this.getStashRootFolder(args.db, true);
        const iModelId = args.db.iModelId;
        const stash = args.db[Symbols_1._nativeDb].stashChanges({ stashRootDir, description: args.description, iModelId });
        if (args.discardLocalChanges) {
            await args.db.discardChanges({ retainLocks: args.retainLocks });
        }
        core_bentley_1.Logger.logInfo(loggerCategory, `Stashed changes`, () => stash);
        return stash;
    }
    /**
     * Retrieves the stash properties from the database for the given arguments.
     *
     * @param args - The arguments required to locate and access the stash.
     * @returns The stash file properties if found; otherwise, `undefined`.
     */
    static tryGetStash(args) {
        try {
            return this.getStash(args);
        }
        catch (error) {
            core_bentley_1.Logger.logError(loggerCategory, `Error getting stash with ${this.getStashId(args)}: ${error.message}`);
        }
        return undefined;
    }
    /**
     * Retrieves the stash properties from the database using the provided arguments.
     *
     * @param args - The arguments required to access the stash.
     * @returns The stash properties parsed from the database.
     */
    static getStash(args) {
        return this.withStash(args, (stashDb) => {
            const stashProps = stashDb.withPreparedSqliteStatement("SELECT [val] FROM [be_Local] WHERE [name]='$stash_info'", (stmt) => {
                if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
                    StashError.throwError("invalid-stash", "Invalid stash");
                return JSON.parse(stmt.getValueString(0));
            });
            return stashProps;
        });
    }
    /**
     * Executes a callback function with a read-only SQLite database connection to a stash file.
     *
     * @typeParam T - The return type of the callback function.
     * @param args - Arguments required to determine the stash file path.
     * @param callback - A function that receives an open {@link SQLiteDb} instance connected to the stash file.
     * @returns The value returned by the callback function.
     */
    static withStash(args, callback) {
        const stashFile = this.getStashFilePath(args);
        if (!(0, node_fs_1.existsSync)(stashFile)) {
            StashError.throwError("invalid-stash", "Invalid stash");
        }
        const stashDb = new SQLiteDb_1.SQLiteDb();
        stashDb.openDb(stashFile, core_bentley_1.OpenMode.Readonly);
        try {
            return callback(stashDb);
        }
        finally {
            stashDb.closeDb();
        }
    }
    /**
     * Retrieves all stash files associated with the specified {@link BriefcaseDb}.
     * @param db - The {@link BriefcaseDb} instance for which to retrieve stash files.
     * @returns An array of `StashProps` representing the found stash files, sorted by timestamp.
     */
    static getStashes(db) {
        const stashes = [];
        const stashDir = this.getStashRootFolder(db, false);
        if (!(0, node_fs_1.existsSync)(stashDir)) {
            return stashes;
        }
        (0, node_fs_1.readdirSync)(stashDir).filter((file) => {
            const filePath = path.join(stashDir, file);
            if ((0, node_fs_1.existsSync)(filePath) && (0, node_fs_1.statSync)(filePath).isFile() && file.endsWith(".stash")) {
                const id = file.slice(0, -path.extname(file).length);
                const stash = this.tryGetStash({ db, stash: id });
                if (stash) {
                    stashes.push(stash);
                }
            }
        });
        stashes.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
        return stashes;
    }
    /**
     * Deletes the stash file associated with the specified stash ID or properties from the given {@link BriefcaseDb}.
     *
     * @param db - The {@link BriefcaseDb} instance from which the stash should be dropped.
     * @param stashId - The unique identifier (GuidString) or properties (StashProps) of the stash to be deleted.
     * @returns Returns `true` if the stash file was successfully deleted, otherwise returns `false`.
     */
    static dropStash(args) {
        try {
            const stashFile = this.getStashFilePath(args);
            (0, node_fs_1.unlinkSync)(stashFile);
            return true;
        }
        catch (error) {
            core_bentley_1.Logger.logError(loggerCategory, `Error dropping stash: ${error}`);
        }
        return false;
    }
    /**
     * Removes all stashes associated with the specified {@link BriefcaseDb}.
     *
     * @param db - The {@link BriefcaseDb} instance from which all stashes will be removed.
     */
    static dropAllStashes(db) {
        this.getStashes(db).forEach((stash) => {
            this.dropStash({ db, stash });
        });
    }
    /**
     * Queries the hub for the changeset information associated with the given stash.
     *
     * @param args - The arguments including the stash properties.
     * @returns A promise resolving to the changeset ID and index.
     */
    static async queryChangeset(args) {
        return IModelHost_1.IModelHost[Symbols_1._hubAccess].queryChangeset({
            iModelId: args.stash.iModelId,
            changeset: args.stash.parentChangeset,
            accessToken: await IModelHost_1.IModelHost.getAccessToken()
        });
    }
    /**
     * Restores the specified stash to the given {@link BriefcaseDb}. This operation will discard any local changes made to db and reverse the tip to the state of the stash and then apply stash. This will restore the undo stack.
     *
     * @param args - The arguments including the target database and stash properties.
     */
    static async restore(args) {
        const { db } = args;
        core_bentley_1.Logger.logInfo(loggerCategory, `Restoring stash: ${this.getStashId(args)}`);
        const stash = this.tryGetStash(args);
        if (!stash) {
            StashError.throwError("stash-not-found", `Stash not found ${this.getStashId(args)}`);
        }
        if (db.txns.hasUnsavedChanges) {
            StashError.throwError("unsaved-changes", `Unsaved changes are present.`);
        }
        if (db.iModelId !== stash.iModelId) {
            StashError.throwError("invalid-stash", `Stash does not belong to this iModel`);
        }
        if (db.briefcaseId !== stash.briefcaseId) {
            StashError.throwError("invalid-stash", `Stash does not belong to this briefcase`);
        }
        const stashFile = this.getStashFilePath({ db, stash });
        // we need to retain lock that overlapped with stash locks instead of all locks
        await db.discardChanges({ retainLocks: true });
        await this.acquireLocks(args);
        if (db.changeset.id !== stash.parentChangeset.id) {
            // Changeset ID mismatch
            core_bentley_1.Logger.logWarning(loggerCategory, "Changeset ID mismatch");
            const stashChangeset = await this.queryChangeset({ db, stash });
            await BriefcaseManager_1.BriefcaseManager.pullAndApplyChangesets(db, { toIndex: stashChangeset.index });
        }
        db[Symbols_1._nativeDb].stashRestore(stashFile);
        db[Symbols_1._resetIModelDb]();
        db[Symbols_1._nativeDb].saveChanges();
        core_bentley_1.Logger.logInfo(loggerCategory, `Restored stash: ${this.getStashId(args)}`);
    }
}
exports.StashManager = StashManager;
//# sourceMappingURL=StashManager.js.map