"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ShellInstanceState = void 0;
const errors_1 = require("@mongosh/errors");
const service_provider_core_1 = require("@mongosh/service-provider-core");
const events_1 = require("events");
const mongodb_redact_1 = require("mongodb-redact");
const decorators_1 = require("./decorators");
const enums_1 = require("./enums");
const error_codes_1 = require("./error-codes");
const index_1 = require("./index");
const interruptor_1 = require("./interruptor");
const mongo_errors_1 = require("./mongo-errors");
const no_db_1 = __importDefault(require("./no-db"));
const shell_bson_1 = require("@mongosh/shell-bson");
const streams_1 = require("./streams");
const shell_log_1 = require("./shell-log");
const mongodb_schema_1 = require("mongodb-schema");
const service_provider_wrapper_1 = require("./deep-inspect/service-provider-wrapper");
const CONTROL_CHAR_REGEXP = /[\x00-\x1F\x7F-\x9F]/g;
class ShellInstanceState {
    constructor(initialServiceProvider, messageBus = new events_1.EventEmitter(), cliOptions = {}, bsonLibrary = initialServiceProvider.bsonLibrary) {
        this.displayBatchSizeFromDBQuery = undefined;
        this.isInteractive = false;
        this.apiCallDepth = 0;
        this.warningsShown = new Set();
        this.interrupted = new interruptor_1.InterruptFlag();
        this.plugins = [new mongo_errors_1.TransformMongoErrorPlugin()];
        this.alreadyTransformedErrors = new WeakMap();
        this.preFetchCollectionAndDatabaseNames = true;
        this.initialServiceProvider =
            cliOptions.deepInspect === false
                ? initialServiceProvider
                : (0, service_provider_wrapper_1.deepInspectServiceProviderWrapper)(initialServiceProvider);
        this.bsonLibrary = bsonLibrary;
        this.messageBus = messageBus;
        this.shellApi = new index_1.ShellApi(this);
        this.shellLog = new shell_log_1.ShellLog(this);
        this.shellBson = this.constructShellBson();
        this.mongos = [];
        this.connectionInfoCache = {
            forSp: this.initialServiceProvider,
            info: undefined,
        };
        if (!cliOptions.nodb) {
            const mongo = new index_1.Mongo(this, undefined, undefined, undefined, this.initialServiceProvider);
            this.mongos.push(mongo);
            this.currentDb = mongo.getDB(this.initialServiceProvider.initialDb || service_provider_core_1.DEFAULT_DB);
        }
        else {
            this.currentDb = new no_db_1.default();
        }
        this.currentCursor = null;
        this.context = {};
        this.cliOptions = cliOptions;
        this.evaluationListener = {};
    }
    constructShellBson() {
        return (0, shell_bson_1.constructShellBson)({
            ...ShellInstanceState.commonConstructShellBsonProps,
            bsonLibrary: this.bsonLibrary,
            printWarning: (msg) => {
                void this.shellApi.print(`Warning: ${msg}`);
            },
        });
    }
    async fetchConnectionInfo() {
        if (!this.cliOptions.nodb) {
            const serviceProvider = this.currentServiceProvider;
            if (serviceProvider === this.connectionInfoCache.forSp &&
                this.connectionInfoCache.info) {
                return this.connectionInfoCache.info;
            }
            const connectionInfoPromise = serviceProvider.getConnectionInfo();
            this.connectionInfoCache = {
                forSp: serviceProvider,
                info: connectionInfoPromise,
            };
            let connectionInfo;
            try {
                connectionInfo = await connectionInfoPromise;
            }
            finally {
                if (this.connectionInfoCache.info === connectionInfoPromise)
                    this.connectionInfoCache.info = connectionInfo;
            }
            const apiVersionInfo = this.apiVersionInfo();
            this.messageBus.emit('mongosh:connect', {
                ...connectionInfo?.extraInfo,
                resolved_hostname: connectionInfo?.resolvedHostname,
                api_version: apiVersionInfo?.version,
                api_strict: apiVersionInfo?.strict,
                api_deprecation_errors: apiVersionInfo?.deprecationErrors,
                uri: (0, mongodb_redact_1.redactConnectionString)(connectionInfo?.extraInfo?.uri ?? ''),
            });
            return connectionInfo;
        }
        return undefined;
    }
    cachedConnectionInfo() {
        const connectionInfo = this.connectionInfoCache.info;
        return ((connectionInfo && 'extraInfo' in connectionInfo && connectionInfo) ||
            undefined);
    }
    async close() {
        for (const mongo of [...this.mongos]) {
            await mongo.close();
        }
    }
    setPreFetchCollectionAndDatabaseNames(value) {
        this.preFetchCollectionAndDatabaseNames = value;
    }
    setDbFunc(newDb) {
        this.currentDb = newDb;
        this.context.rs = new index_1.ReplicaSet(this.currentDb);
        this.context.sh = new index_1.Shard(this.currentDb);
        this.context.sp = streams_1.Streams.newInstance(this.currentDb);
        this.fetchConnectionInfo().catch((err) => this.messageBus.emit('mongosh:error', err, 'shell-api'));
        if (this.preFetchCollectionAndDatabaseNames) {
            this.currentDb
                ._getCollectionNamesForCompletion()
                .catch((err) => this.messageBus.emit('mongosh:error', err, 'shell-api'));
            this.currentDb._mongo
                ._getDatabaseNamesForCompletion()
                .catch((err) => this.messageBus.emit('mongosh:error', err, 'shell-api'));
        }
        this.currentCursor = null;
        return newDb;
    }
    setCtx(contextObject) {
        this.context = contextObject;
        Object.assign(contextObject, this.shellApi);
        contextObject.log = this.shellLog;
        for (const name of Object.getOwnPropertyNames(index_1.ShellApi.prototype)) {
            const { shellApi } = this;
            if (decorators_1.toIgnore.concat(['help']).includes(name) ||
                typeof shellApi[name] !== 'function') {
                continue;
            }
            contextObject[name] = function (...args) {
                return shellApi[name](...args);
            };
            contextObject[name].help = shellApi[name].help;
        }
        contextObject.help = this.shellApi.help;
        Object.assign(contextObject, this.shellBson);
        if (contextObject.console === undefined) {
            contextObject.console = {};
        }
        for (const key of ['log', 'warn', 'info', 'error']) {
            contextObject.console[key] = (...args) => {
                return contextObject.print(...args);
            };
        }
        contextObject.console.clear = contextObject.cls;
        contextObject.rs = new index_1.ReplicaSet(this.currentDb);
        contextObject.sh = new index_1.Shard(this.currentDb);
        contextObject.sp = streams_1.Streams.newInstance(this.currentDb);
        const setFunc = (newDb) => {
            if ((0, index_1.getShellApiType)(newDb) !== 'Database') {
                throw new errors_1.MongoshInvalidInputError("Cannot reassign 'db' to non-Database type", errors_1.CommonErrors.InvalidOperation);
            }
            return this.setDbFunc(newDb);
        };
        if (this.initialServiceProvider.platform === 'JavaShell') {
            contextObject.db = this.setDbFunc(this.currentDb);
        }
        else {
            Object.defineProperty(contextObject, 'db', {
                configurable: true,
                set: setFunc,
                get: () => this.currentDb,
            });
        }
        this.messageBus.emit('mongosh:setCtx', { method: 'setCtx', arguments: {} });
    }
    get currentServiceProvider() {
        try {
            return (this.currentDb._mongo._serviceProvider ?? this.initialServiceProvider);
        }
        catch (err) {
            if (err?.code === error_codes_1.ShellApiErrors.NotConnected) {
                return this.initialServiceProvider;
            }
            throw err;
        }
    }
    emitApiCallWithArgs(event) {
        this.messageBus.emit('mongosh:api-call-with-arguments', event);
    }
    emitApiCall(event) {
        this.messageBus.emit('mongosh:api-call', {
            ...event,
            callDepth: this.apiCallDepth,
        });
    }
    setEvaluationListener(listener) {
        this.evaluationListener = listener;
    }
    getMongoByConnectionId(connectionId) {
        for (const mongo of this.mongos) {
            if (mongo._getConnectionId() === connectionId) {
                return mongo;
            }
        }
        throw new Error(`mongo with connection id ${connectionId} not found`);
    }
    getAutocompletionContext() {
        return {
            currentDatabaseAndConnection: () => {
                try {
                    return {
                        connectionId: this.currentDb.getMongo()._getConnectionId(),
                        databaseName: this.currentDb.getName(),
                    };
                }
                catch (err) {
                    if (err.name === 'MongoshInvalidInputError') {
                        return undefined;
                    }
                    throw err;
                }
            },
            databasesForConnection: async (connectionId) => {
                const mongo = this.getMongoByConnectionId(connectionId);
                try {
                    const dbNames = await mongo._getDatabaseNamesForCompletion();
                    return dbNames.filter((name) => !CONTROL_CHAR_REGEXP.test(name));
                }
                catch (err) {
                    if (err?.code === error_codes_1.ShellApiErrors.NotConnected ||
                        err?.codeName === 'Unauthorized') {
                        return [];
                    }
                    throw err;
                }
            },
            collectionsForDatabase: async (connectionId, databaseName) => {
                const mongo = this.getMongoByConnectionId(connectionId);
                try {
                    const collectionNames = await mongo
                        ._getDb(databaseName)
                        ._getCollectionNamesForCompletion();
                    return collectionNames.filter((name) => !CONTROL_CHAR_REGEXP.test(name));
                }
                catch (err) {
                    if (err?.code === error_codes_1.ShellApiErrors.NotConnected ||
                        err?.codeName === 'Unauthorized') {
                        return [];
                    }
                    throw err;
                }
            },
            schemaInformationForCollection: async (connectionId, databaseName, collectionName) => {
                const mongo = this.getMongoByConnectionId(connectionId);
                let docs = [];
                if ((await this.evaluationListener.getConfig?.('disableSchemaSampling')) !== true) {
                    try {
                        docs = await mongo
                            ._getDb(databaseName)
                            .getCollection(collectionName)
                            ._getSampleDocsForCompletion();
                    }
                    catch (err) {
                        if (err?.code !== error_codes_1.ShellApiErrors.NotConnected &&
                            err?.codeName !== 'Unauthorized') {
                            throw err;
                        }
                    }
                }
                const schemaAccessor = await (0, mongodb_schema_1.analyzeDocuments)(docs);
                const schema = await schemaAccessor.getMongoDBJsonSchema();
                return schema;
            },
        };
    }
    getAutocompleteParameters() {
        return {
            topology: () => {
                let topology;
                const topologyDescription = this.currentServiceProvider.getTopologyDescription();
                if (!topologyDescription)
                    return undefined;
                const topologyType = topologyDescription?.type;
                switch (topologyType) {
                    case 'ReplicaSetNoPrimary':
                    case 'ReplicaSetWithPrimary':
                        topology = 'ReplSet';
                        break;
                    case 'Sharded':
                        topology = 'Sharded';
                        break;
                    case 'LoadBalanced':
                        topology = 'LoadBalanced';
                        break;
                    default:
                        topology = 'Standalone';
                        if (topologyDescription?.servers?.size === 1) {
                            const [server] = topologyDescription?.servers.values();
                            switch (server.type) {
                                case 'Mongos':
                                    topology = 'Sharded';
                                    break;
                                case 'PossiblePrimary':
                                case 'RSPrimary':
                                case 'RSSecondary':
                                case 'RSArbiter':
                                case 'RSOther':
                                case 'RSGhost':
                                    topology = 'ReplSet';
                                    break;
                                default:
                                    break;
                            }
                        }
                        break;
                }
                return topology;
            },
            apiVersionInfo: () => {
                return this.apiVersionInfo();
            },
            connectionInfo: () => {
                return this.cachedConnectionInfo()?.extraInfo ?? undefined;
            },
            getCollectionCompletionsForCurrentDb: async (collName) => {
                try {
                    const collectionNames = await this.currentDb._getCollectionNamesForCompletion();
                    const result = collectionNames.filter((name) => name.toLowerCase().startsWith(collName.toLowerCase()) &&
                        !CONTROL_CHAR_REGEXP.test(name));
                    this.messageBus.emit('mongosh:load-collections-complete');
                    return result;
                }
                catch (err) {
                    if (err?.code === error_codes_1.ShellApiErrors.NotConnected ||
                        err?.codeName === 'Unauthorized') {
                        return [];
                    }
                    throw err;
                }
            },
            getDatabaseCompletions: async (dbName) => {
                try {
                    const dbNames = await this.currentDb._mongo._getDatabaseNamesForCompletion();
                    const result = dbNames.filter((name) => name.toLowerCase().startsWith(dbName.toLowerCase()) &&
                        !CONTROL_CHAR_REGEXP.test(name));
                    this.messageBus.emit('mongosh:load-databases-complete');
                    return result;
                }
                catch (err) {
                    if (err?.code === error_codes_1.ShellApiErrors.NotConnected ||
                        err?.codeName === 'Unauthorized') {
                        return [];
                    }
                    throw err;
                }
            },
        };
    }
    apiVersionInfo() {
        const { serverApi } = this.currentServiceProvider.getRawClient()?.options ?? {};
        return serverApi?.version
            ? { strict: false, deprecationErrors: false, ...serverApi }
            : undefined;
    }
    async onInterruptExecution() {
        await this.interrupted.set();
        this.currentCursor = null;
        this.resumeMongosAfterInterrupt = await Promise.all(this.mongos.map(async (m) => {
            try {
                return {
                    mongo: m,
                    resume: await m._suspend(),
                };
            }
            catch (e) {
                return {
                    mongo: m,
                    resume: null,
                };
            }
        }));
        return !this.resumeMongosAfterInterrupt.find((r) => r.resume === null);
    }
    async onResumeExecution() {
        const promises = this.resumeMongosAfterInterrupt?.map(async (r) => {
            if (!this.mongos.find((m) => m === r.mongo)) {
                return true;
            }
            if (r.resume === null) {
                return false;
            }
            try {
                await r.resume();
                return true;
            }
            catch (e) {
                return false;
            }
        }) ?? [];
        this.resumeMongosAfterInterrupt = undefined;
        const result = await Promise.all(promises);
        this.interrupted.reset();
        return !result.find((r) => r === false);
    }
    async getDefaultPrompt() {
        const connectionInfo = await this.fetchConnectionInfo();
        if (connectionInfo?.extraInfo?.is_stream) {
            return 'AtlasStreamProcessing> ';
        }
        const prefix = await this.getDefaultPromptPrefix();
        const topologyInfo = await this.getTopologySpecificPrompt();
        let dbname = '';
        try {
            dbname = this.currentDb.getName();
        }
        catch {
        }
        return `${[prefix, topologyInfo, dbname].filter(Boolean).join(' ')}> `;
    }
    async getDefaultPromptPrefix() {
        const connectionInfo = await this.fetchConnectionInfo();
        const extraConnectionInfo = connectionInfo?.extraInfo;
        if (extraConnectionInfo?.is_data_federation) {
            return 'AtlasDataFederation';
        }
        else if (extraConnectionInfo?.is_local_atlas) {
            return 'AtlasLocalDev';
        }
        else if (extraConnectionInfo?.is_atlas) {
            return 'Atlas';
        }
        else if (extraConnectionInfo?.is_enterprise ||
            connectionInfo?.buildInfo?.modules?.indexOf('enterprise') >= 0) {
            return 'Enterprise';
        }
        return '';
    }
    async getTopologySpecificPrompt() {
        const connectionInfo = await this.fetchConnectionInfo();
        const description = this.currentServiceProvider.getTopologyDescription();
        if (!description) {
            return '';
        }
        let replicaSet = description.setName;
        let serverTypePrompt = '';
        switch (description.type) {
            case 'Single': {
                const singleDetails = this.getTopologySinglePrompt(description);
                replicaSet = singleDetails?.replicaSet ?? replicaSet;
                serverTypePrompt = singleDetails?.serverType
                    ? `[direct: ${singleDetails.serverType}]`
                    : '';
                break;
            }
            case 'ReplicaSetNoPrimary':
                serverTypePrompt = '[secondary]';
                break;
            case 'ReplicaSetWithPrimary':
                serverTypePrompt = '[primary]';
                break;
            case 'Sharded':
                serverTypePrompt = connectionInfo?.extraInfo?.atlas_version
                    ? ''
                    : '[mongos]';
                break;
            case 'LoadBalanced':
            default:
                return '';
        }
        const setNamePrefix = replicaSet ? `${replicaSet} ` : '';
        return `${setNamePrefix}${serverTypePrompt}`;
    }
    getTopologySinglePrompt(description) {
        if (description.servers?.size !== 1) {
            return undefined;
        }
        const [server] = description.servers.values();
        let serverType;
        switch (server.type) {
            case 'Mongos':
                serverType = 'mongos';
                break;
            case 'RSPrimary':
                serverType = 'primary';
                break;
            case 'RSSecondary':
                serverType = 'secondary';
                break;
            case 'RSArbiter':
                serverType = 'arbiter';
                break;
            case 'RSOther':
                serverType = 'other';
                break;
            default:
                serverType = '';
        }
        return {
            replicaSet: server.setName ?? description.setName ?? null,
            serverType,
        };
    }
    registerPlugin(plugin) {
        this.plugins.push(plugin);
    }
    transformError(err) {
        if (Object.prototype.toString.call(err) === '[object Error]') {
            if (this.alreadyTransformedErrors.has(err)) {
                return this.alreadyTransformedErrors.get(err);
            }
            const before = err;
            for (const plugin of this.plugins) {
                if (plugin.transformError) {
                    err = plugin.transformError(err);
                }
            }
            this.alreadyTransformedErrors.set(before, err);
        }
        return err;
    }
    async printDeprecationWarning(message) {
        if (!this.warningsShown.has(message)) {
            this.warningsShown.add(message);
            if (this.context.print) {
                await this.context.print.call(this.shellApi, `DeprecationWarning: ${message}`);
            }
            else {
                await this.shellApi.print(`DeprecationWarning: ${message}`);
            }
        }
    }
    async printWarning(message) {
        if (!this.warningsShown.has(message)) {
            this.warningsShown.add(message);
            if (this.context.print) {
                await this.context.print.call(this.shellApi, `Warning: ${message}`);
            }
            else {
                await this.shellApi.print(`Warning: ${message}`);
            }
        }
    }
}
exports.ShellInstanceState = ShellInstanceState;
ShellInstanceState.commonConstructShellBsonProps = {
    assignMetadata: (target, { help, maxVersion, minVersion, deprecated }) => {
        target.serverVersions =
            maxVersion || minVersion
                ? [
                    minVersion ?? enums_1.ServerVersions.earliest,
                    maxVersion ?? enums_1.ServerVersions.latest,
                ]
                : enums_1.ALL_SERVER_VERSIONS;
        target.platforms = enums_1.ALL_PLATFORMS;
        target.topologies = enums_1.ALL_TOPOLOGIES;
        if (deprecated)
            target.deprecated = true;
        if (help) {
            target.help = () => help;
            Object.setPrototypeOf(target.help, help);
        }
    },
    constructHelp: (className) => {
        const classHelpKeyPrefix = `shell-api.classes.${className}.help`;
        const classHelp = {
            help: `${classHelpKeyPrefix}.description`,
            example: `${classHelpKeyPrefix}.example`,
            docs: `${classHelpKeyPrefix}.link`,
            attr: [],
        };
        return new index_1.Help(classHelp);
    },
};
exports.default = ShellInstanceState;
//# sourceMappingURL=shell-instance-state.js.map