/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module IModelHost
 */
// To avoid circular load errors, the "Element" classes must be loaded before IModelHost.
import "./IModelDb"; // DO NOT REMOVE OR MOVE THIS LINE!
import { IModelNative, loadNativePlatform } from "./internal/NativePlatform";
import * as os from "os";
import "reflect-metadata"; // this has to be before @itwin/object-storage-* and @itwin/cloud-agnostic-core imports because those packages contain decorators that use this polyfill.
import { NativeLibrary } from "@bentley/imodeljs-native";
import { Types as ExtensionTypes } from "@itwin/cloud-agnostic-core";
import { assert, BeEvent, BentleyStatus, DbResult, Guid, IModelStatus, Logger, ProcessDetector } from "@itwin/core-bentley";
import { IModelError } from "@itwin/core-common";
import { AzureServerStorageBindings } from "@itwin/object-storage-azure";
import { ServerStorage } from "@itwin/object-storage-core";
import { BackendLoggerCategory } from "./BackendLoggerCategory";
import { BisCoreSchema } from "./BisCoreSchema";
import { BriefcaseManager } from "./BriefcaseManager";
import { CloudSqlite } from "./CloudSqlite";
import { FunctionalSchema } from "./domains/FunctionalSchema";
import { GenericSchema } from "./domains/GenericSchema";
import { GeoCoordConfig } from "./GeoCoordConfig";
import { IModelJsFs } from "./IModelJsFs";
import { DevToolsRpcImpl } from "./rpc-impl/DevToolsRpcImpl";
import { IModelReadRpcImpl } from "./rpc-impl/IModelReadRpcImpl";
import { IModelTileRpcImpl } from "./rpc-impl/IModelTileRpcImpl";
import { SnapshotIModelRpcImpl } from "./rpc-impl/SnapshotIModelRpcImpl";
import { initializeRpcBackend } from "./RpcBackend";
import { TileStorage } from "./TileStorage";
import { SettingsPriority } from "./workspace/Settings";
import { Container } from "inversify";
import { join, normalize as normalizeDir } from "path";
import { constructWorkspace } from "./internal/workspace/WorkspaceImpl";
import { SettingsImpl } from "./internal/workspace/SettingsImpl";
import { constructSettingsSchemas } from "./internal/workspace/SettingsSchemasImpl";
import { _getHubAccess, _hubAccess, _setHubAccess } from "./internal/Symbols";
const loggerCategory = BackendLoggerCategory.IModelHost;
/** Configuration of core-backend.
 * @public
 */
export class IModelHostConfiguration {
    static defaultTileRequestTimeout = 20 * 1000;
    static defaultLogTileLoadTimeThreshold = 40;
    static defaultLogTileSizeThreshold = 20 * 1000000;
    /** @internal */
    static defaultMaxTileCacheDbSize = 1024 * 1024 * 1024;
    appAssetsDir;
    cacheDir;
    /** @beta */
    workspace;
    hubAccess;
    /** The AuthorizationClient used to obtain [AccessToken]($bentley)s. */
    authorizationClient;
    /** @beta */
    restrictTileUrlsByClientIp;
    compressCachedTiles;
    /** @beta */
    tileCacheAzureCredentials;
    /** @internal */
    tileTreeRequestTimeout = IModelHostConfiguration.defaultTileRequestTimeout;
    /** @internal */
    tileContentRequestTimeout = IModelHostConfiguration.defaultTileRequestTimeout;
    /** @internal */
    logTileLoadTimeThreshold = IModelHostConfiguration.defaultLogTileLoadTimeThreshold;
    /** @internal */
    logTileSizeThreshold = IModelHostConfiguration.defaultLogTileSizeThreshold;
    /** @internal */
    crashReportingConfig;
    /**
     * Configuration controlling whether to use the thinned down native instance functions for element, model, and aspect CRUD operations
     * or use the previous behavior of using the native side for all CRUD operations. Set to true to revert to the previous behavior.
     * @beta
    */
    disableThinnedNativeInstanceWorkflow;
    /**
     * Configuration controlling whether to disable the creation of restore points during pull/merge operations.
     * @beta
     */
    disableRestorePointOnPullMerge;
}
/**
 * Settings for `IModelHost.appWorkspace`.
 * @note this includes the default dictionary from the SettingsSpecRegistry
 */
class ApplicationSettings extends SettingsImpl {
    _remove;
    verifyPriority(priority) {
        if (priority > SettingsPriority.application) // only application or lower may appear in ApplicationSettings
            throw new Error("Use IModelSettings");
    }
    updateDefaults() {
        const defaults = {};
        for (const [schemaName, val] of IModelHost.settingsSchemas.settingDefs) {
            if (val.default)
                defaults[schemaName] = val.default;
        }
        this.addDictionary({ name: "_default_", priority: 0 }, defaults);
    }
    constructor() {
        super();
        this._remove = IModelHost.settingsSchemas.onSchemaChanged.addListener(() => this.updateDefaults());
        this.updateDefaults();
    }
    close() {
        if (this._remove) {
            this._remove();
            this._remove = undefined;
        }
    }
}
const definedInStartup = (obj) => {
    if (obj === undefined)
        throw new Error("IModelHost.startup must be called first");
    return obj;
};
/** IModelHost initializes ($backend) and captures its configuration. A backend must call [[IModelHost.startup]] before using any backend classes.
 * See [the learning article]($docs/learning/backend/IModelHost.md)
 * @public
 */
export class IModelHost {
    constructor() { }
    /** The AuthorizationClient used to obtain [AccessToken]($bentley)s. */
    static authorizationClient;
    static backendVersion = "";
    static _profileName;
    static _cacheDir = "";
    static _settingsSchemas;
    static _appWorkspace;
    // Omit the hubAccess field from configuration so it stays internal.
    static configuration;
    /**
     * The name of the *Profile* directory (a subdirectory of "[[cacheDir]]/profiles/") for this process.
     *
     * The *Profile* directory is used to cache data that is specific to a type-of-usage of the iTwin.js library.
     * It is important that information in the profile cache be consistent but isolated across sessions (i.e.
     * data for a profile is maintained between runs, but each profile is completely independent and
     * unaffected by the presence or use of others.)
     * @note **Only one process at a time may be using a given profile**, and an exception will be thrown by [[startup]]
     * if a second process attempts to use the same profile.
     * @beta
     */
    static get profileName() {
        return this._profileName;
    }
    /** The full path of the Profile directory.
     * @see [[profileName]]
     * @beta
     */
    static get profileDir() {
        return join(this._cacheDir, "profiles", this._profileName);
    }
    /** Event raised during startup to allow loading settings data */
    static onWorkspaceStartup = new BeEvent();
    /** Event raised just after the backend IModelHost was started */
    static onAfterStartup = new BeEvent();
    /** Event raised just before the backend IModelHost is to be shut down */
    static onBeforeShutdown = new BeEvent();
    /** @internal */
    static session = { applicationId: "2686", applicationVersion: "1.0.0", sessionId: "" };
    /** A uniqueId for this session */
    static get sessionId() { return this.session.sessionId; }
    static set sessionId(id) { this.session.sessionId = id; }
    /** The Id of this application - needs to be set only if it is an agent application. The applicationId will otherwise originate at the frontend. */
    static get applicationId() { return this.session.applicationId; }
    static set applicationId(id) { this.session.applicationId = id; }
    /** The version of this application - needs to be set if is an agent application. The applicationVersion will otherwise originate at the frontend. */
    static get applicationVersion() { return this.session.applicationVersion; }
    static set applicationVersion(version) { this.session.applicationVersion = version; }
    /** A string that can identify the current user to other users when collaborating. */
    static userMoniker = "unknown";
    /** Root directory holding files that iTwin.js caches */
    static get cacheDir() { return this._cacheDir; }
    /** The application [[Workspace]] for this `IModelHost`
     * @note this `Workspace` only holds [[WorkspaceContainer]]s and [[Settings]] scoped to the currently loaded application(s).
     * All organization, iTwin, and iModel based containers or settings must be accessed through [[IModelDb.workspace]] and
     * attempting to add them to this Workspace will fail.
     * @beta
     */
    static get appWorkspace() { return definedInStartup(this._appWorkspace); }
    /** The registry of schemas describing the [[Setting]]s for the application session.
     * Applications should register their schemas via methods like [[SettingsSchemas.addGroup]].
     * @beta
     */
    static get settingsSchemas() { return definedInStartup(this._settingsSchemas); }
    /** The optional [[FileNameResolver]] that resolves keys and partial file names for snapshot iModels.
     * @deprecated in 4.10 - will not be removed until after 2026-06-13. When opening a snapshot by file name, ensure to pass already resolved path. Using a key to open a snapshot is now deprecated.
     */
    static snapshotFileNameResolver; // eslint-disable-line @typescript-eslint/no-deprecated
    /** Get the current access token for this IModelHost, or a blank string if none is available.
     * @note for web backends, this will *always* return a blank string because the backend itself has no token (but never needs one either.)
     * For all IpcHosts, where this backend is servicing a single frontend, this will be the user's token. For ElectronHost, the backend
     * obtains the token and forwards it to the frontend.
     * @note accessTokens expire periodically and are automatically refreshed, if possible. Therefore tokens should not be saved, and the value
     * returned by this method may change over time throughout the course of a session.
     */
    static async getAccessToken() {
        try {
            return (await IModelHost.authorizationClient?.getAccessToken()) ?? "";
        }
        catch {
            return "";
        }
    }
    static loadNative(options) {
        loadNativePlatform();
        if (options.crashReportingConfig && options.crashReportingConfig.crashDir && !ProcessDetector.isElectronAppBackend && !ProcessDetector.isMobileAppBackend) {
            IModelNative.platform.setCrashReporting(options.crashReportingConfig);
            Logger.logTrace(loggerCategory, "Configured crash reporting", {
                enableCrashDumps: options.crashReportingConfig?.enableCrashDumps,
                wantFullMemoryDumps: options.crashReportingConfig?.wantFullMemoryDumps,
                enableNodeReport: options.crashReportingConfig?.enableNodeReport,
                uploadToBentley: options.crashReportingConfig?.uploadToBentley,
            });
            if (options.crashReportingConfig.enableNodeReport) {
                if (process.report !== undefined) {
                    process.report.reportOnFatalError = true;
                    process.report.reportOnUncaughtException = true;
                    process.report.directory = options.crashReportingConfig.crashDir;
                    Logger.logTrace(loggerCategory, "Configured Node.js crash reporting");
                }
                else {
                    Logger.logWarning(loggerCategory, "Unable to configure Node.js crash reporting");
                }
            }
        }
    }
    /** @internal */
    static tileStorage;
    static _hubAccess;
    /** @internal */
    static [_setHubAccess](hubAccess) { this._hubAccess = hubAccess; }
    /** get the current hubAccess, if present.
     * @internal
     */
    static [_getHubAccess]() { return this._hubAccess; }
    /** Provides access to the IModelHub for this IModelHost
     * @internal
     * @note If [[IModelHostOptions.hubAccess]] was undefined when initializing this class, accessing this property will throw an error.
     * To determine whether one is present, use [[_getHubAccess]].
     */
    static get [_hubAccess]() {
        if (IModelHost._hubAccess === undefined)
            throw new IModelError(IModelStatus.BadRequest, "No BackendHubAccess supplied in IModelHostOptions");
        return IModelHost._hubAccess;
    }
    static initializeWorkspace(configuration) {
        const settingAssets = join(KnownLocations.packageAssetsDir, "Settings");
        this._settingsSchemas = constructSettingsSchemas();
        this._settingsSchemas.addDirectory(join(settingAssets, "Schemas"));
        this._appWorkspace = constructWorkspace(new ApplicationSettings(), configuration.workspace);
        // Create the CloudCache for Workspaces. This will fail if another process is already using the same profile.
        try {
            this.appWorkspace.getCloudCache();
        }
        catch (e) {
            throw (e.errorNumber === DbResult.BE_SQLITE_BUSY) ? new IModelError(DbResult.BE_SQLITE_BUSY, `Profile [${this.profileDir}] is already in use by another process`) : e;
        }
        this.appWorkspace.settings.addDirectory(settingAssets, SettingsPriority.defaults);
        GeoCoordConfig.onStartup();
        // allow applications to load their default settings
        this.onWorkspaceStartup.raiseEvent();
    }
    static _isValid = false;
    /** true between a successful call to [[startup]] and before [[shutdown]] */
    static get isValid() {
        return IModelHost._isValid;
    }
    /** This method must be called before any iTwin.js services are used.
     * @param options Host configuration data.
     * Raises [[onAfterStartup]].
     * @see [[shutdown]].
     */
    static async startup(options) {
        if (this._isValid)
            return; // we're already initialized
        this._isValid = true;
        options = options ?? {};
        if (this.sessionId === "")
            this.sessionId = Guid.createValue();
        this.authorizationClient = options.authorizationClient;
        this.backendVersion = require("../../package.json").version; // eslint-disable-line @typescript-eslint/no-require-imports
        initializeRpcBackend(options.enableOpenTelemetry);
        this.loadNative(options);
        this.setupCacheDir(options);
        this.initializeWorkspace(options);
        BriefcaseManager.initialize(join(this._cacheDir, "imodels"));
        [
            IModelReadRpcImpl,
            IModelTileRpcImpl,
            SnapshotIModelRpcImpl, // eslint-disable-line @typescript-eslint/no-deprecated
            DevToolsRpcImpl,
        ].forEach((rpc) => rpc.register()); // register all of the RPC implementations
        [
            BisCoreSchema,
            GenericSchema,
            FunctionalSchema,
        ].forEach((schema) => schema.registerSchema()); // register all of the schemas
        const { hubAccess, ...otherOptions } = options;
        if (undefined !== hubAccess)
            this._hubAccess = hubAccess;
        this.configuration = otherOptions;
        this.setupTileCache();
        process.once("beforeExit", IModelHost.shutdown);
        this.onAfterStartup.raiseEvent();
    }
    static setupCacheDir(configuration) {
        this._cacheDir = normalizeDir(configuration.cacheDir ?? NativeLibrary.defaultCacheDir);
        IModelJsFs.recursiveMkDirSync(this._cacheDir);
        this._profileName = configuration.profileName ?? "default";
        Logger.logInfo(loggerCategory, `cacheDir: [${this.cacheDir}], profileDir: [${this.profileDir}]`);
    }
    /** This method must be called when an iTwin.js host is shut down. Raises [[onBeforeShutdown]] */
    static async shutdown() {
        // Note: This method is set as a node listener where `this` is unbound. Call private method to
        // ensure `this` is correct. Don't combine these methods.
        return IModelHost.doShutdown();
    }
    /**
     * Create a new iModel.
     * @returns the Guid of the newly created iModel.
     * @throws [IModelError]($common) in case of errors.
     * @note If [[IModelHostOptions.hubAccess]] was undefined in the call to [[startup]], this function will throw an error.
     */
    static async createNewIModel(arg) {
        return this[_hubAccess].createNewIModel(arg);
    }
    static async doShutdown() {
        if (!this._isValid)
            return;
        this._isValid = false;
        this.onBeforeShutdown.raiseEvent();
        this.configuration = undefined;
        this.tileStorage = undefined;
        this._appWorkspace?.close();
        this._appWorkspace = undefined;
        this._settingsSchemas = undefined;
        CloudSqlite.CloudCaches.destroy();
        process.removeListener("beforeExit", IModelHost.shutdown);
    }
    /**
     * Add or update a property that should be included in a crash report.
     * @internal
     */
    static setCrashReportProperty(name, value) {
        IModelNative.platform.setCrashReportProperty(name, value);
    }
    /**
     * Remove a previously defined property so that will not be included in a crash report.
     * @internal
     */
    static removeCrashReportProperty(name) {
        IModelNative.platform.setCrashReportProperty(name, undefined);
    }
    /**
     * Get all properties that will be included in a crash report.
     * @internal
     */
    static getCrashReportProperties() {
        return IModelNative.platform.getCrashReportProperties();
    }
    /** The directory where application assets may be found */
    static get appAssetsDir() {
        return undefined !== IModelHost.configuration ? IModelHost.configuration.appAssetsDir : undefined;
    }
    /** The time, in milliseconds, for which IModelTileRpcInterface.requestTileTreeProps should wait before returning a "pending" status.
     * @internal
     */
    static get tileTreeRequestTimeout() {
        return IModelHost.configuration?.tileTreeRequestTimeout ?? IModelHostConfiguration.defaultTileRequestTimeout;
    }
    /** The time, in milliseconds, for which IModelTileRpcInterface.requestTileContent should wait before returning a "pending" status.
     * @internal
     */
    static get tileContentRequestTimeout() {
        return IModelHost.configuration?.tileContentRequestTimeout ?? IModelHostConfiguration.defaultTileRequestTimeout;
    }
    /** The backend will log when a tile took longer to load than this threshold in seconds. */
    static get logTileLoadTimeThreshold() {
        return IModelHost.configuration?.logTileLoadTimeThreshold ?? IModelHostConfiguration.defaultLogTileLoadTimeThreshold;
    }
    /** The backend will log when a tile is loaded with a size in bytes above this threshold. */
    static get logTileSizeThreshold() {
        return IModelHost.configuration?.logTileSizeThreshold ?? IModelHostConfiguration.defaultLogTileSizeThreshold;
    }
    /** Whether external tile caching is active.
     * @internal
     */
    static get usingExternalTileCache() {
        return undefined !== IModelHost.tileStorage;
    }
    /** Whether to restrict tile cache URLs by client IP address.
     * @internal
     */
    static get restrictTileUrlsByClientIp() {
        return undefined !== IModelHost.configuration && (IModelHost.configuration.restrictTileUrlsByClientIp ? true : false);
    }
    /** Whether to compress cached tiles.
     * @internal
     */
    static get compressCachedTiles() {
        return false !== IModelHost.configuration?.compressCachedTiles;
    }
    static setupTileCache() {
        assert(undefined !== IModelHost.configuration);
        const config = IModelHost.configuration;
        const storage = config.tileCacheStorage;
        const credentials = config.tileCacheAzureCredentials;
        if (!storage && !credentials) {
            IModelNative.platform.setMaxTileCacheSize(config.maxTileCacheDbSize ?? IModelHostConfiguration.defaultMaxTileCacheDbSize);
            return;
        }
        IModelNative.platform.setMaxTileCacheSize(0);
        if (credentials) {
            if (storage)
                throw new IModelError(BentleyStatus.ERROR, "Cannot use both Azure and custom cloud storage providers for tile cache.");
            this.setupAzureTileCache(credentials);
        }
        if (storage)
            IModelHost.tileStorage = new TileStorage(storage);
    }
    static setupAzureTileCache(credentials) {
        const config = {
            // eslint-disable-next-line @typescript-eslint/naming-convention
            ServerSideStorage: {
                dependencyName: "azure",
                accountName: credentials.account,
                accountKey: credentials.accessKey,
                baseUrl: credentials.baseUrl ?? `https://${credentials.account}.blob.core.windows.net`,
            },
        };
        const ioc = new Container();
        ioc.bind(ExtensionTypes.dependenciesConfig).toConstantValue(config);
        new AzureServerStorageBindings().register(ioc, config.ServerSideStorage);
        IModelHost.tileStorage = new TileStorage(ioc.get(ServerStorage));
    }
    /** @internal */
    static computeSchemaChecksum(arg) {
        return IModelNative.platform.computeSchemaChecksum(arg);
    }
}
/** Information about the platform on which the app is running.
 * @public
 */
export class Platform {
    /** Get the name of the platform. */
    static get platformName() {
        return process.platform;
    }
}
/** Well known directories that may be used by the application.
 * @public
 */
export class KnownLocations {
    /** The directory where the imodeljs-native assets are stored. */
    static get nativeAssetsDir() {
        return IModelNative.platform.DgnDb.getAssetsDir();
    }
    /** The directory where the core-backend assets are stored. */
    static get packageAssetsDir() {
        return join(__dirname, "assets");
    }
    /** The temporary directory. */
    static get tmpdir() {
        return os.tmpdir();
    }
}
/** Extend this class to provide custom file name resolution behavior.
 * @note Only `tryResolveKey` and/or `tryResolveFileName` need to be overridden as the implementations of `resolveKey` and `resolveFileName` work for most purposes.
 * @see [[IModelHost.snapshotFileNameResolver]]
 * @public
 * @deprecated in 4.10 - will not be removed until after 2026-06-13. When opening a snapshot by file name, ensure to pass already resolved path. Using a key to open a snapshot is now deprecated.
 */
export class FileNameResolver {
    /** Resolve a file name from the specified key.
     * @param _fileKey The key that identifies the file name in a `Map` or other similar data structure.
     * @returns The resolved file name or `undefined` if not found.
     */
    tryResolveKey(_fileKey) { return undefined; }
    /** Resolve a file name from the specified key.
     * @param fileKey The key that identifies the file name in a `Map` or other similar data structure.
     * @returns The resolved file name.
     * @throws [[IModelError]] if not found.
     */
    resolveKey(fileKey) {
        const resolvedFileName = this.tryResolveKey(fileKey);
        if (undefined === resolvedFileName) {
            throw new IModelError(IModelStatus.NotFound, `${fileKey} not resolved`);
        }
        return resolvedFileName;
    }
    /** Resolve the input file name, which may be a partial name, into a full path file name.
     * @param inFileName The partial file name.
     * @returns The resolved full path file name or `undefined` if not found.
     */
    tryResolveFileName(inFileName) { return inFileName; }
    /** Resolve the input file name, which may be a partial name, into a full path file name.
     * @param inFileName The partial file name.
     * @returns The resolved full path file name.
     * @throws [[IModelError]] if not found.
     */
    resolveFileName(inFileName) {
        const resolvedFileName = this.tryResolveFileName(inFileName);
        if (undefined === resolvedFileName) {
            throw new IModelError(IModelStatus.NotFound, `${inFileName} not resolved`);
        }
        return resolvedFileName;
    }
}
//# sourceMappingURL=IModelHost.js.map