"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MongoDBAutocompleter = void 0;
const debug_1 = __importDefault(require("debug"));
const lodash_1 = __importDefault(require("lodash"));
const ts_autocomplete_1 = __importDefault(require("@mongodb-js/ts-autocomplete"));
const autocomplete_types_1 = __importDefault(require("./types/autocomplete-types"));
const api_1 = require("@mongosh/shell-api/api");
const utils_1 = require("./utils");
const mongodb_schema_1 = require("mongodb-schema");
const cdt_analyser_1 = require("./cdt-analyser");
const autocompletion_context_1 = require("./autocompletion-context");
const debug = (0, debug_1.default)('mongodb-ts-autocomplete');
class DatabaseSchema {
    collectionSchemas;
    constructor() {
        this.collectionSchemas = Object.create(null);
    }
    setCollectionNames(collectionNames) {
        let changed = false;
        for (const collectionName of collectionNames) {
            if (!this.collectionSchemas[collectionName]) {
                this.collectionSchemas[collectionName] = undefined;
                changed = true;
            }
        }
        const knownCollectionNames = new Set(collectionNames);
        for (const key of Object.keys(this.collectionSchemas)) {
            if (!knownCollectionNames.has(key)) {
                delete this.collectionSchemas[key];
                changed = true;
            }
        }
        return changed;
    }
    setCollectionSchema(collectionName, schema) {
        const isChanged = lodash_1.default.isEqual(this.collectionSchemas[collectionName], schema);
        this.collectionSchemas[collectionName] = schema;
        return isChanged;
    }
    toTypescriptTypeDefinition() {
        const collectionProperties = Object.entries(this.collectionSchemas).map(([collectionName, schema]) => {
            const def = schema ? (0, mongodb_schema_1.toTypescriptTypeDefinition)(schema) : `{}`;
            const lines = def.split(/\n/g).map((line) => `      ${line}`);
            return `  ${JSON.stringify(collectionName)}: {
      schema: ${lines.join('\n').trim()}
    };`;
        });
        return `{
  ${collectionProperties.join('\n')}
  }`;
    }
}
class ConnectionSchema {
    databaseSchemas;
    constructor() {
        this.databaseSchemas = Object.create(null);
    }
    addDatabase(databaseName) {
        if (!this.databaseSchemas[databaseName]) {
            this.databaseSchemas[databaseName] = new DatabaseSchema();
        }
    }
    setDatabaseCollectionNames(databaseName, collectionNames) {
        this.addDatabase(databaseName);
        return this.databaseSchemas[databaseName].setCollectionNames(collectionNames);
    }
    addCollectionSchema(databaseName, collectionName, collectionSchema) {
        this.addDatabase(databaseName);
        return this.databaseSchemas[databaseName].setCollectionSchema(collectionName, collectionSchema);
    }
    toTypescriptTypeDefinition() {
        const databaseProperties = Object.entries(this.databaseSchemas).map(([databaseName, schema]) => {
            const def = schema.toTypescriptTypeDefinition();
            return `${JSON.stringify(databaseName)}: ${def}`;
        });
        return `{
  ${databaseProperties.join('\n')}
}`;
    }
}
function filterStartingWith({ kind, name, trigger, }) {
    name = name.toLocaleLowerCase();
    trigger = trigger.toLocaleLowerCase();
    return ((trigger !== '' || kind === 'property' || kind === 'method') &&
        name.startsWith(trigger));
}
class MongoDBAutocompleter {
    context;
    connectionSchemas;
    autocompleter;
    previousConnectionDB;
    previousCollectionName;
    constructor({ context, fallbackServiceHost }) {
        this.context = autocompletion_context_1.CachingAutocompletionContext.caching(context);
        this.autocompleter = new ts_autocomplete_1.default({
            filter: filterStartingWith,
            fallbackServiceHost,
        });
        this.connectionSchemas = Object.create(null);
        this.autocompleter.updateCode({
            ...autocomplete_types_1.default,
        });
    }
    addConnection(connectionId) {
        if (!this.connectionSchemas[connectionId]) {
            this.connectionSchemas[connectionId] = new ConnectionSchema();
        }
        return this.connectionSchemas[connectionId];
    }
    getConnectionSchemaCode(connectionId, schemaType) {
        return `
import * as bson from '/bson.ts';
import * as mql from '/mql.ts';

export type ServerSchema = ${this.connectionSchemas[connectionId].toTypescriptTypeDefinition()};

export type ConnectionMQLQuery = mql.Query<${schemaType}>;
export type ConnectionMQLPipeline = mql.Pipeline<${schemaType}>;
export type ConnectionMQLDocument = ${schemaType};
`;
    }
    getConnectionShellAPICode(connectionId) {
        return `
/// <reference types="node" />
import {ConnectionMQLQuery, ConnectionMQLPipeline, ConnectionMQLDocument} from '/${connectionId}-schema.ts';
${adjustShellApiForConnection(api_1.api)}
`;
    }
    getCurrentGlobalsCode(connectionId, databaseName) {
        return `
import * as mql from '/mql.ts';
import {
  ServerSchema,
} from '/${connectionId}-schema.ts';
import {
  DatabaseWithSchema,
  ReplicaSet,
  Shard,
  Streams
} from '/${connectionId}-shell-api.ts';

type CurrentDatabaseSchema = ServerSchema[${JSON.stringify(databaseName)}];

declare global {
  const db: DatabaseWithSchema<ServerSchema, CurrentDatabaseSchema>;
  const rs: ReplicaSet<ServerSchema, CurrentDatabaseSchema>;
  const sh: Shard<ServerSchema, CurrentDatabaseSchema>;
  const sp: Streams<ServerSchema, CurrentDatabaseSchema>;
}
`;
    }
    async autocomplete(code) {
        const dbAndConnection = this.context.currentDatabaseAndConnection();
        if (!dbAndConnection) {
            this.previousConnectionDB = undefined;
            this.previousCollectionName = undefined;
            return [];
        }
        const isConnectionDBChanged = lodash_1.default.isEqual(this.previousConnectionDB, dbAndConnection);
        this.previousConnectionDB = dbAndConnection;
        const { connectionId, databaseName } = dbAndConnection;
        const tsAst = (0, cdt_analyser_1.compileSourceFile)(code);
        const collectionName = (0, cdt_analyser_1.inferCollectionNameFromFunctionCall)(tsAst) || 'test';
        const isCollectionNameChanged = this.previousCollectionName !== collectionName;
        this.previousCollectionName = collectionName;
        const schema = await this.context.schemaInformationForCollection(connectionId, databaseName, collectionName);
        const connection = this.addConnection(connectionId);
        const isSchemaChanged = connection.addCollectionSchema(databaseName, collectionName, schema);
        const collectionNames = await this.context.collectionsForDatabase(connectionId, databaseName);
        const isCollectionsChanged = connection.setDatabaseCollectionNames(databaseName, collectionNames);
        if (isSchemaChanged || isCollectionsChanged) {
            const schemaType = collectionNames.includes(collectionName)
                ? `ServerSchema[${JSON.stringify(databaseName)}][${JSON.stringify(collectionName)}]['schema']`
                : '{}';
            this.autocompleter.updateCode({
                [`/${connectionId}-schema.ts`]: this.getConnectionSchemaCode(connectionId, schemaType),
                [`/${connectionId}-shell-api.ts`]: this.getConnectionShellAPICode(connectionId),
            });
        }
        if (isSchemaChanged ||
            isCollectionsChanged ||
            isConnectionDBChanged ||
            isCollectionNameChanged) {
            this.autocompleter.updateCode({
                '/current-globals.ts': this.getCurrentGlobalsCode(connectionId, databaseName),
            });
        }
        debug({
            connectionId,
            databaseName,
            collectionName,
            isSchemaChanged,
            isCollectionsChanged,
            isConnectionDBChanged,
            isCollectionNameChanged,
            code,
        });
        return this.autocompleter.autocomplete(code);
    }
}
exports.MongoDBAutocompleter = MongoDBAutocompleter;
function adjustShellApiForConnection(ShellApiText) {
    const replacements = [
        [/type MQLQuery = Document.*;/, 'type MQLQuery = ConnectionMQLQuery;'],
        [
            /type MQLPipeline = Document.*;/,
            'type MQLPipeline = ConnectionMQLPipeline;',
        ],
        [
            /type MQLDocument = Document.*;/,
            'type MQLDocument = ConnectionMQLDocument;',
        ],
    ];
    let result = (0, utils_1.replaceImports)(ShellApiText);
    for (const [searchValue, replaceValue] of replacements) {
        if (!result.search(searchValue)) {
            throw new Error(`Expected shell-api to contain ${searchValue.toString()}`);
        }
        result = result.replace(searchValue, replaceValue);
    }
    return result;
}
//# sourceMappingURL=autocompleter.js.map