/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Elements
 */
import { DbResult, IModelStatus } from "@itwin/core-bentley";
import { ChannelControlError, IModel, IModelError } from "@itwin/core-common";
import { ChannelControl } from "../ChannelControl";
import { Subject } from "../Element";
import { IModelHost } from "../IModelHost";
import { ElementOwnsChannelRootAspect } from "../NavigationRelationship";
import { _implementationProhibited, _nativeDb, _verifyChannel } from "./Symbols";
import * as semver from "semver";
class ChannelAdmin {
    _iModel;
    static channelClassName = "bis:ChannelRootAspect";
    [_implementationProhibited] = undefined;
    _allowedChannels = new Set();
    _allowedModels = new Set();
    _deniedModels = new Map();
    constructor(_iModel) {
        this._iModel = _iModel;
        // for backwards compatibility, allow the shared channel unless explicitly turned off in IModelHostOptions.
        if (IModelHost.configuration?.allowSharedChannel !== false)
            this._allowedChannels.add(ChannelControl.sharedChannelName);
    }
    addAllowedChannel(channelKey) {
        this._allowedChannels.add(channelKey);
        this._deniedModels.clear();
    }
    removeAllowedChannel(channelKey) {
        this._allowedChannels.delete(channelKey);
        this._allowedModels.clear();
    }
    getChannelKey(elementId) {
        if (elementId === IModel.rootSubjectId)
            return ChannelControl.sharedChannelName;
        try {
            // eslint-disable-next-line @typescript-eslint/no-deprecated
            const channel = this._iModel.withPreparedStatement(`SELECT Owner FROM ${ChannelAdmin.channelClassName} WHERE Element.Id=?`, (stmt) => {
                stmt.bindId(1, elementId);
                return DbResult.BE_SQLITE_ROW === stmt.step() ? stmt.getValue(0).getString() : undefined;
            });
            if (channel !== undefined)
                return channel;
        }
        catch {
            // Exception happens if the iModel is too old: ChannelRootAspect class not present in the BisCore schema (older than v1.0.10).
            // In that case all data in such iModel is assumed to be in the shared channel.
            return ChannelControl.sharedChannelName;
        }
        const parentId = this._iModel.withPreparedSqliteStatement("SELECT ParentId,ModelId FROM bis_Element WHERE id=?", (stmt) => {
            stmt.bindId(1, elementId);
            if (DbResult.BE_SQLITE_ROW !== stmt.step())
                throw new IModelError(IModelStatus.NotFound, "Element does not exist");
            return stmt.getValueId(0) ?? stmt.getValueId(1); // if parent is undefined, use modelId
        });
        return this.getChannelKey(parentId);
    }
    [_verifyChannel](modelId) {
        // Note: indirect changes are permitted to change any channel
        if (this._allowedModels.has(modelId) || this._iModel[_nativeDb].getTxnMode() === "indirect")
            return;
        const deniedChannel = this._deniedModels.get(modelId);
        if (undefined !== deniedChannel)
            ChannelControlError.throwError("not-allowed", `Channel ${deniedChannel} is not allowed`, deniedChannel);
        const channel = this.getChannelKey(modelId);
        if (this._allowedChannels.has(channel)) {
            this._allowedModels.add(modelId);
            return;
        }
        this._deniedModels.set(modelId, channel);
        return this[_verifyChannel](modelId);
    }
    makeChannelRoot(args) {
        const channelKey = this.getChannelKey(args.elementId);
        if (ChannelControl.sharedChannelName !== channelKey)
            ChannelControlError.throwError("may-not-nest", `Channel ${channelKey} may not nest`, channelKey);
        if (this.queryChannelRoot(args.channelKey) !== undefined)
            ChannelControlError.throwError("root-exists", `Channel ${args.channelKey} root already exist`, channelKey);
        const props = {
            classFullName: ChannelAdmin.channelClassName,
            element: {
                id: args.elementId,
                relClassName: ElementOwnsChannelRootAspect.classFullName,
            },
            owner: args.channelKey,
        };
        this._iModel.elements.insertAspect(props);
    }
    insertChannelSubject(args) {
        // Check if channelKey already exists before inserting Subject.
        // makeChannelRoot will check that again, but at that point the new Subject is already inserted.
        // Prefer to check twice instead of deleting the Subject in the latter option.
        if (this.queryChannelRoot(args.channelKey) !== undefined)
            ChannelControlError.throwError("root-exists", `Channel ${args.channelKey} root already exist`, args.channelKey);
        const elementId = Subject.insert(this._iModel, args.parentSubjectId ?? IModel.rootSubjectId, args.subjectName, args.description);
        this.makeChannelRoot({ elementId, channelKey: args.channelKey });
        return elementId;
    }
    queryChannelRoot(channelKey) {
        if (channelKey === ChannelControl.sharedChannelName)
            // RootSubject acts as the ChannelRoot element of the shared channel
            return IModel.rootSubjectId;
        try {
            // eslint-disable-next-line @typescript-eslint/no-deprecated
            const channelRoot = this._iModel.withPreparedStatement(`SELECT Element.Id FROM ${ChannelAdmin.channelClassName} WHERE Owner=?`, (stmt) => {
                stmt.bindString(1, channelKey);
                return DbResult.BE_SQLITE_ROW === stmt.step() ? stmt.getValue(0).getId() : undefined;
            });
            return channelRoot;
        }
        catch {
            // Exception happens if the iModel is too old: ChannelRootAspect class not present in the BisCore schema (older than v1.0.10).
            // In that case all data in such iModel is assumed to be in the shared channel.
            return undefined;
        }
    }
    async upgradeChannel(options, iModel, data) {
        // Validations
        if (!this._allowedChannels.has(options.channelKey))
            ChannelControlError.throwError("not-allowed", `Channel ${options.channelKey} is not allowed`, options.channelKey);
        if (semver.gte(options.fromVersion, options.toVersion))
            ChannelControlError.throwError("not-allowed", `Upgrading channel ${options.channelKey} from ${options.fromVersion} to ${options.toVersion} is not allowed`, options.channelKey);
        if (!this.queryChannelRoot(options.channelKey) && options.channelKey !== ChannelControl.sharedChannelName)
            ChannelControlError.throwError("not-allowed", `Channel ${options.channelKey} not found`, options.channelKey);
        const context = {
            iModel,
            channelKey: options.channelKey,
            fromVersion: options.fromVersion,
            toVersion: options.toVersion,
            data,
        };
        try {
            await options.callback(context);
        }
        catch (error) {
            ChannelControlError.throwError(error, "channel-upgrade-failed", `Channel ${options.channelKey} upgrade failed: ${error.message}`);
        }
    }
}
export function createChannelControl(iModel) {
    return new ChannelAdmin(iModel);
}
//# sourceMappingURL=ChannelAdmin.js.map