/*---------------------------------------------------------------------------------------------
* 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
 */
import * as fs from "fs";
import { join } from "path";
import { CloudSqlite } from "./CloudSqlite";
import { IModelHost, KnownLocations } from "./IModelHost";
import { BriefcaseIdValue, CatalogError, CloudSqliteError } from "@itwin/core-common";
import { IpcHandler } from "./IpcHost";
import { StandaloneDb } from "./IModelDb";
import { Guid, OpenMode } from "@itwin/core-bentley";
import { _nativeDb } from "./internal/Symbols";
import { IModelNative } from "./internal/NativePlatform";
let readonlyCloudCache;
let writeableCloudCache;
const catalogManifestName = "CatalogManifest";
// we make a readonly CloudCache and a writeable CloudCache. That way the access token authorizations are distinct.
function makeCloudCache(arg, writeable) {
    const cache = CloudSqlite.CloudCaches.getCache(arg);
    // if the cache was just created, add the "catalog" members as hidden
    if (undefined === cache.catalogContainers) {
        CloudSqlite.addHiddenProperty(cache, "catalogContainers", new Map());
        CloudSqlite.addHiddenProperty(cache, "writeable", writeable);
    }
    return cache;
}
// find an existing CloudContainer for accessing a CatalogIModel, or make a new one and connect it
async function getCatalogContainerObj(cache, containerId) {
    const cloudContainer = cache.catalogContainers.get(containerId);
    if (undefined !== cloudContainer)
        return cloudContainer;
    const accessLevel = cache.writeable ? "write" : "read";
    const tokenProps = await CloudSqlite.getBlobService().requestToken({ containerId, accessLevel, userToken: await IModelHost.getAccessToken() });
    const container = CloudSqlite.createCloudContainer({
        accessLevel,
        baseUri: tokenProps.baseUri,
        containerId,
        storageType: tokenProps.provider,
        writeable: cache.writeable,
        accessToken: tokenProps.token
    });
    cache.catalogContainers.set(containerId, container); // save the container in the map of ContainerIds so we can reuse them
    container.connect(cache);
    return container;
}
function getReadonlyCloudCache() { return readonlyCloudCache ??= makeCloudCache({ cacheName: "catalogs", cacheSize: "10G" }, false); }
;
function getWritableCloudCache() { return writeableCloudCache ??= makeCloudCache({ cacheName: "writeableCatalogs", cacheSize: "10G" }, true); }
;
async function getReadonlyContainer(containerId) { return getCatalogContainerObj(getReadonlyCloudCache(), containerId); }
;
async function getWriteableContainer(containerId) { return getCatalogContainerObj(getWritableCloudCache(), containerId); }
;
/** Throw an error if the write lock is not held for the supplied container */
function ensureLocked(container, reason) {
    if (!container.hasWriteLock)
        CloudSqliteError.throwError("write-lock-not-held", { message: `Write lock must be held to ${reason}` });
}
/** update the manifest in a CatalogIModel (calls `saveChanges`) */
function updateManifest(nativeDb, manifest) {
    nativeDb.saveLocalValue(catalogManifestName, JSON.stringify(manifest));
    nativeDb.saveChanges("update manifest");
}
function catalogDbNameWithDefault(dbName) {
    return dbName ?? "catalog-db";
}
/** A StandaloneDb that holds a CatalogIModel */
class CatalogDbImpl extends StandaloneDb {
    isEditable() {
        return false;
    }
    getManifest() {
        const manifestString = this[_nativeDb].queryLocalValue(catalogManifestName);
        if (undefined === manifestString)
            return undefined;
        return JSON.parse(manifestString);
    }
    getVersion() {
        return CloudSqlite.parseDbFileName(this[_nativeDb].getFilePath()).version;
    }
    getInfo() {
        return { manifest: this.getManifest(), version: this.getVersion() };
    }
}
/**
 * A CatalogDb that permits editing.
 * This class ensures that CatalogIModels never have a Txn table when they are published.
 * It also automatically updates the `lastEditedBy` field in the CatalogManifest.
 */
class EditableCatalogDbImpl extends CatalogDbImpl {
    isEditable() {
        return true;
    }
    updateCatalogManifest(manifest) {
        updateManifest(this[_nativeDb], manifest);
    }
    // Make sure the txn table is deleted and update the manifest every time we close.
    beforeClose() {
        try {
            const manifest = this.getManifest();
            const container = this.cloudContainer;
            if (container && manifest) {
                manifest.lastEditedBy = CloudSqlite.getWriteLockHeldBy(container);
                this.updateCatalogManifest(manifest);
            }
            // when saved, CatalogIModels should never have any Txns. If we wanted to create a changeset, we'd have to do it here.
            this[_nativeDb].deleteAllTxns();
        }
        catch { } // ignore errors attempting to update
        // might also want to vacuum here?
        super.beforeClose();
    }
}
function findCatalogByKey(key) {
    return CatalogDbImpl.findByKey(key);
}
/** @beta */
export var CatalogDb;
(function (CatalogDb) {
    /** Create a new [[BlobContainer]] to hold versions of a [[CatalogDb]].
     * @returns The properties of the newly created container.
     * @note creating new containers requires "admin" authorization.
    */
    async function createNewContainer(args) {
        const dbName = catalogDbNameWithDefault(args.dbName);
        CloudSqlite.validateDbName(dbName);
        CloudSqlite.validateDbVersion(args.version);
        const tmpName = join(KnownLocations.tmpdir, `temp-${dbName}`);
        try {
            // make a copy of the file they supplied so we can modify its contents safely
            fs.copyFileSync(args.localCatalogFile, tmpName);
            const nativeDb = new IModelNative.platform.DgnDb();
            nativeDb.openIModel(tmpName, OpenMode.ReadWrite);
            nativeDb.setITwinId(Guid.empty); // catalogs must be a StandaloneDb
            nativeDb.setIModelId(Guid.createValue()); // make sure its iModelId is unique
            updateManifest(nativeDb, args.manifest); // store the manifest inside the Catalog
            nativeDb.deleteAllTxns(); // Catalogs should never have Txns (and, this must be empty before resetting BriefcaseId)
            nativeDb.resetBriefcaseId(BriefcaseIdValue.Unassigned); // catalogs should always be unassigned
            nativeDb.saveChanges(); // save change to briefcaseId
            nativeDb.vacuum();
            nativeDb.closeFile();
        }
        catch (e) {
            CatalogError.throwError("invalid-seed-catalog", { message: "Illegal seed catalog", ...args, cause: e });
        }
        const userToken = await IModelHost.getAccessToken();
        // create tne new container from the blob service, requires "admin" authorization
        const cloudContainerProps = await CloudSqlite.getBlobService().create({ scope: { iTwinId: args.iTwinId }, metadata: { ...args.metadata, containerType: "CatalogIModel" }, userToken });
        // now create a CloudSqlite container object to access it
        const container = CloudSqlite.createCloudContainer({
            accessToken: await CloudSqlite.requestToken(cloudContainerProps),
            accessLevel: "admin",
            writeable: true,
            baseUri: cloudContainerProps.baseUri,
            containerId: cloudContainerProps.containerId,
            storageType: cloudContainerProps.provider,
        });
        // initialize the container for use by CloudSqlite
        container.initializeContainer({ blockSize: 4 * 1024 * 1024 });
        container.connect(getWritableCloudCache());
        // upload the initial version of the Catalog
        await CloudSqlite.withWriteLock({ user: "initialize", container }, async () => {
            await CloudSqlite.uploadDb(container, { dbName: CloudSqlite.makeSemverName(dbName, args.version), localFileName: tmpName });
            fs.unlinkSync(tmpName); // delete temporary copy of catalog
        });
        container.disconnect();
        return cloudContainerProps;
    }
    CatalogDb.createNewContainer = createNewContainer;
    /** Acquire the write lock for a [CatalogIModel]($common) container. Only one person may obtain the write lock at a time.
     * You must obtain the lock before attempting to write to the container via functions like [[CatalogDb.openEditable]] and [[CatalogDb.createNewVersion]].
     * @note This requires "write" authorization to the container
     */
    async function acquireWriteLock(args) {
        const container = await getWriteableContainer(args.containerId);
        return CloudSqlite.acquireWriteLock({ container, user: args.username });
    }
    CatalogDb.acquireWriteLock = acquireWriteLock;
    /** Release the write lock on a [CatalogIModel]($common) container. This uploads all changes made while the lock is held, so they become visible to other users. */
    async function releaseWriteLock(args) {
        const container = await getWriteableContainer(args.containerId);
        if (args.abandon)
            container.abandonChanges();
        CloudSqlite.releaseWriteLock(container);
    }
    CatalogDb.releaseWriteLock = releaseWriteLock;
    /** Open an [[EditableCatalogDb]] for write access.
     * @note Once a version of a catalog iModel has been published (i.e. the write lock has been released), it is no longer editable, *unless* it is a prerelease version.
     * @note The write lock must be held for this operation to succeed
     */
    async function openEditable(args) {
        const dbName = catalogDbNameWithDefault(args.dbName);
        if (undefined === args.containerId) // local file?
            return EditableCatalogDbImpl.openFile(dbName, OpenMode.ReadWrite, args);
        const container = await getWriteableContainer(args.containerId);
        ensureLocked(container, "open a Catalog for editing"); // editing Catalogs requires the write lock
        // look up the full name with version
        const dbFullName = CloudSqlite.querySemverMatch({ container, dbName, version: args.version ?? "*" });
        if (!CloudSqlite.isSemverEditable(dbFullName, container))
            CloudSqliteError.throwError("already-published", { message: "Catalog has already been published and is not editable. Make a new version first.", ...args });
        if (args.prefetch)
            CloudSqlite.startCloudPrefetch(container, dbFullName);
        return EditableCatalogDbImpl.openFile(dbFullName, OpenMode.ReadWrite, { container, ...args });
    }
    CatalogDb.openEditable = openEditable;
    /** Open a [[CatalogDb]] for read-only access. */
    async function openReadonly(args) {
        const dbName = catalogDbNameWithDefault(args.dbName);
        if (undefined === args.containerId) // local file?
            return CatalogDbImpl.openFile(dbName, OpenMode.Readonly, args);
        const container = await getReadonlyContainer(args.containerId);
        if (args.syncWithCloud)
            container.checkForChanges();
        const dbFullName = CloudSqlite.querySemverMatch({ container, ...args, dbName });
        if (args.prefetch)
            CloudSqlite.startCloudPrefetch(container, dbFullName);
        return CatalogDbImpl.openFile(dbFullName, OpenMode.Readonly, { container, ...args });
    }
    CatalogDb.openReadonly = openReadonly;
    /**
     * Create a new version of a [CatalogIModel]($common) as a copy of an existing version. Immediately after this operation, the new version will be an exact copy
     * of the source CatalogIModel. Then, use [[CatalogDb.openEditable]] to modify the new version with new content.
     * @note The write lock must be held for this operation to succeed
     */
    async function createNewVersion(args) {
        const container = await getWriteableContainer(args.containerId);
        ensureLocked(container, "create a new version");
        return CloudSqlite.createNewDbVersion(container, { ...args, fromDb: { ...args.fromDb, dbName: catalogDbNameWithDefault(args.fromDb.dbName) } });
    }
    CatalogDb.createNewVersion = createNewVersion;
})(CatalogDb || (CatalogDb = {}));
/**
 * Handler for Ipc access to CatalogIModels. Registered by NativeHost.
 * @internal
 */
export class CatalogIModelHandler extends IpcHandler {
    get channelName() { return "catalogIModel/ipc"; }
    async createNewContainer(args) {
        return CatalogDb.createNewContainer(args);
    }
    async acquireWriteLock(args) {
        return CatalogDb.acquireWriteLock(args);
    }
    async releaseWriteLock(args) {
        return CatalogDb.releaseWriteLock(args);
    }
    async openReadonly(args) {
        return ((await CatalogDb.openReadonly(args)).getConnectionProps());
    }
    async openEditable(args) {
        return (await CatalogDb.openEditable(args)).getConnectionProps();
    }
    async createNewVersion(args) {
        return CatalogDb.createNewVersion(args);
    }
    async getInfo(key) {
        return findCatalogByKey(key).getInfo();
    }
    async updateCatalogManifest(key, manifest) {
        findCatalogByKey(key).updateCatalogManifest(manifest);
    }
}
//# sourceMappingURL=CatalogDb.js.map