"use strict";
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var InlineEditsModelService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.UndesiredModels = exports.InlineEditsModelService = void 0;
const arrays_1 = require("../../../util/common/arrays");
const async_1 = require("../../../util/common/async");
const errors_1 = require("../../../util/common/errors");
const arrays_2 = require("../../../util/vs/base/common/arrays");
const assert_1 = require("../../../util/vs/base/common/assert");
const event_1 = require("../../../util/vs/base/common/event");
const lifecycle_1 = require("../../../util/vs/base/common/lifecycle");
const observable_1 = require("../../../util/vs/base/common/observable");
const copilotTokenStore_1 = require("../../authentication/common/copilotTokenStore");
const configurationService_1 = require("../../configuration/common/configurationService");
const extensionContext_1 = require("../../extContext/common/extensionContext");
const logService_1 = require("../../log/common/logService");
const proxyModelsService_1 = require("../../proxyModels/common/proxyModelsService");
const nullExperimentationService_1 = require("../../telemetry/common/nullExperimentationService");
const telemetry_1 = require("../../telemetry/common/telemetry");
const xtabPromptOptions_1 = require("../common/dataTypes/xtabPromptOptions");
const inlineEditsModelService_1 = require("../common/inlineEditsModelService");
let InlineEditsModelService = class InlineEditsModelService extends lifecycle_1.Disposable {
    static { InlineEditsModelService_1 = this; }
    static { this.COPILOT_NES_XTAB_MODEL = {
        modelName: 'copilot-nes-xtab',
        promptingStrategy: xtabPromptOptions_1.PromptingStrategy.CopilotNesXtab,
        includeTagsInCurrentFile: true,
        source: "hardCodedDefault" /* ModelSource.HardCodedDefault */,
        lintOptions: undefined,
    }; }
    static { this.COPILOT_NES_OCT = {
        modelName: 'copilot-nes-oct',
        promptingStrategy: xtabPromptOptions_1.PromptingStrategy.Xtab275,
        includeTagsInCurrentFile: false,
        source: "hardCodedDefault" /* ModelSource.HardCodedDefault */,
        lintOptions: undefined,
    }; }
    static { this.COPILOT_NES_CALLISTO = {
        modelName: 'nes-callisto',
        promptingStrategy: xtabPromptOptions_1.PromptingStrategy.Xtab275,
        includeTagsInCurrentFile: false,
        source: "hardCodedDefault" /* ModelSource.HardCodedDefault */,
        lintOptions: undefined,
    }; }
    constructor(_tokenStore, _proxyModelsService, _undesiredModelsManager, _configService, _expService, _telemetryService, _logService) {
        super();
        this._tokenStore = _tokenStore;
        this._proxyModelsService = _proxyModelsService;
        this._undesiredModelsManager = _undesiredModelsManager;
        this._configService = _configService;
        this._expService = _expService;
        this._telemetryService = _telemetryService;
        this._logService = _logService;
        this._copilotTokenObs = (0, observable_1.observableFromEvent)(this, this._tokenStore.onDidStoreUpdate, () => this._tokenStore.copilotToken);
        // TODO@ulugbekna: use a derived observable such that it fires only when nesModels change
        this._fetchedModelsObs = (0, observable_1.observableFromEvent)(this, this._proxyModelsService.onModelListUpdated, () => this._proxyModelsService.nesModels);
        this._preferredModelNameObs = this._configService.getExperimentBasedConfigObservable(configurationService_1.ConfigKey.Advanced.InlineEditsPreferredModel, this._expService);
        this._localModelConfigObs = this._configService.getConfigObservable(configurationService_1.ConfigKey.Advanced.InlineEditsXtabProviderModelConfiguration);
        this._expBasedModelConfigObs = this._configService.getExperimentBasedConfigObservable(configurationService_1.ConfigKey.TeamInternal.InlineEditsXtabProviderModelConfigurationString, this._expService);
        this._defaultModelConfigObs = this._configService.getExperimentBasedConfigObservable(configurationService_1.ConfigKey.TeamInternal.InlineEditsXtabProviderDefaultModelConfigurationString, this._expService);
        this._useSlashModelsObs = this._configService.getExperimentBasedConfigObservable(configurationService_1.ConfigKey.TeamInternal.InlineEditsUseSlashModels, this._expService);
        this._undesiredModelsObs = (0, observable_1.observableFromEvent)(this, this._undesiredModelsManager.onDidChange, () => this._undesiredModelsManager);
        this._setModelQueue = new async_1.TaskQueue();
        this._logger = _logService.createSubLogger(['NES', 'ModelsService']);
        const logger = this._logger.createSubLogger('constructor');
        this._modelsObs = (0, observable_1.derived)((reader) => {
            logger.trace('computing models');
            return this.aggregateModels({
                copilotToken: this._copilotTokenObs.read(reader),
                fetchedNesModels: this._fetchedModelsObs.read(reader),
                localModelConfig: this._localModelConfigObs.read(reader),
                modelConfigString: this._expBasedModelConfigObs.read(reader),
                defaultModelConfigString: this._defaultModelConfigObs.read(reader),
                useSlashModels: this._useSlashModelsObs.read(reader),
            });
        }).recomputeInitiallyAndOnChange(this._store);
        this._currentModelObs = (0, observable_1.derived)((reader) => {
            logger.trace('computing current model');
            const undesiredModelsManager = this._undesiredModelsObs.read(reader);
            return this._pickModel({
                preferredModelName: this._preferredModelNameObs.read(reader),
                models: this._modelsObs.read(reader),
                undesiredModelsManager,
            });
        }).recomputeInitiallyAndOnChange(this._store);
        this._modelInfoObs = (0, observable_1.derived)((reader) => {
            logger.trace('computing model info');
            return {
                models: this._modelsObs.read(reader),
                currentModelId: this._currentModelObs.read(reader).modelName,
            };
        }).recomputeInitiallyAndOnChange(this._store);
        this.onModelListUpdated = event_1.Event.fromObservableLight(this._modelInfoObs);
    }
    get modelInfo() {
        const models = this._modelsObs.get().map(m => ({
            id: m.modelName,
            name: m.modelName,
        }));
        const currentModel = this._currentModelObs.get();
        return {
            models,
            currentModelId: currentModel.modelName,
        };
    }
    setCurrentModelId(newPreferredModelId) {
        return this._setModelQueue.schedule(() => this._setCurrentModelIdCore(newPreferredModelId));
    }
    async _setCurrentModelIdCore(newPreferredModelId) {
        const currentPreferredModelId = this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.Advanced.InlineEditsPreferredModel, this._expService);
        const isSameModel = currentPreferredModelId === newPreferredModelId;
        if (isSameModel) {
            return;
        }
        // snapshot before async calls
        const currentPreferredModel = this._currentModelObs.get();
        const models = this._modelsObs.get();
        const newPreferredModel = models.find(m => m.modelName === newPreferredModelId);
        if (newPreferredModel === undefined) {
            this._logService.error(`New preferred model id ${newPreferredModelId} not found in model list.`);
            return;
        }
        // if currently selected model is from exp config, then mark that model as undesired
        if (currentPreferredModel.source === "expConfig" /* ModelSource.ExpConfig */) {
            await this._undesiredModelsManager.addUndesiredModelId(currentPreferredModel.modelName);
        }
        if (this._undesiredModelsManager.isUndesiredModelId(newPreferredModelId)) {
            await this._undesiredModelsManager.removeUndesiredModelId(newPreferredModelId);
        }
        // if user picks same as the default model, we should reset the user setting
        // otherwise, update the model
        const expectedDefaultModel = this._pickModel({ preferredModelName: 'none', models, undesiredModelsManager: this._undesiredModelsManager });
        if (newPreferredModel.source === "expConfig" /* ModelSource.ExpConfig */ || // because exp-configured model already takes highest priority
            (newPreferredModelId === expectedDefaultModel.modelName && !models.some(m => m.source === "expConfig" /* ModelSource.ExpConfig */))) {
            this._logger.trace(`New preferred model id ${newPreferredModelId} is the same as the default model, resetting user setting.`);
            await this._configService.setConfig(configurationService_1.ConfigKey.Advanced.InlineEditsPreferredModel, 'none');
        }
        else {
            this._logger.trace(`New preferred model id ${newPreferredModelId} is different from the default model, updating user setting to ${newPreferredModelId}.`);
            await this._configService.setConfig(configurationService_1.ConfigKey.Advanced.InlineEditsPreferredModel, newPreferredModelId);
        }
    }
    aggregateModels({ copilotToken, fetchedNesModels, localModelConfig, modelConfigString, defaultModelConfigString, useSlashModels, }) {
        const logger = this._logger.createSubLogger('aggregateModels');
        const models = [];
        // priority of adding models to the list:
        // 0. model from user local setting
        // 1. model from modelConfigurationString setting (set through ExP)
        // 2. fetched models from /models endpoint (if useSlashModels is true)
        if (localModelConfig) {
            if (models.some(m => m.modelName === localModelConfig.modelName)) {
                logger.trace('Local model configuration already exists in the model list, skipping.');
            }
            else {
                logger.trace(`Adding local model configuration: ${localModelConfig.modelName}`);
                models.push({ ...localModelConfig, source: "localConfig" /* ModelSource.LocalConfig */ });
            }
        }
        if (modelConfigString) {
            logger.trace('Parsing modelConfigurationString...');
            const parsedConfig = this.parseModelConfigString(modelConfigString, configurationService_1.ConfigKey.TeamInternal.InlineEditsXtabProviderModelConfigurationString);
            if (parsedConfig && !models.some(m => m.modelName === parsedConfig.modelName)) {
                logger.trace(`Adding model from modelConfigurationString: ${parsedConfig.modelName}`);
                models.push({ ...parsedConfig, source: "expConfig" /* ModelSource.ExpConfig */ });
            }
            else {
                logger.trace('No valid model found in modelConfigurationString.');
            }
        }
        if (useSlashModels && fetchedNesModels && fetchedNesModels.length > 0) {
            logger.trace(`Processing ${fetchedNesModels.length} fetched models...`);
            const filteredFetchedModels = (0, arrays_1.filterMap)(fetchedNesModels, (m) => {
                if (!(0, xtabPromptOptions_1.isPromptingStrategy)(m.capabilities.promptStrategy)) {
                    return undefined;
                }
                if (models.some(knownModel => knownModel.modelName === m.name)) {
                    logger.trace(`Fetched model ${m.name} already exists in the model list, skipping.`);
                    return undefined;
                }
                return {
                    modelName: m.name,
                    promptingStrategy: m.capabilities.promptStrategy,
                    includeTagsInCurrentFile: false, // FIXME@ulugbekna: determine this based on model capabilities and config
                    source: "fetched" /* ModelSource.Fetched */,
                    lintOptions: undefined,
                };
            });
            logger.trace(`Adding ${filteredFetchedModels.length} fetched models after filtering.`);
            (0, arrays_2.pushMany)(models, filteredFetchedModels);
        }
        else {
            // push default model if /models doesn't give us any models
            logger.trace(`adding built-in default model: useSlashModels ${useSlashModels}, fetchedNesModels ${fetchedNesModels?.length ?? 'undefined'}`);
            const defaultModel = this.determineDefaultModel(copilotToken, defaultModelConfigString);
            if (defaultModel) {
                if (models.some(m => m.modelName === defaultModel.modelName)) {
                    logger.trace('Default model configuration already exists in the model list, skipping.');
                }
                else {
                    logger.trace(`Adding default model configuration: ${defaultModel.modelName}`);
                    models.push(defaultModel);
                }
            }
        }
        return models;
    }
    selectedModelConfiguration() {
        return toModelConfiguration(this._currentModelObs.get());
    }
    defaultModelConfiguration() {
        const models = this._modelsObs.get();
        if (models && models.length > 0) {
            const defaultModels = models.filter(m => !this.isConfiguredModel(m));
            if (defaultModels.length > 0) {
                return toModelConfiguration(defaultModels[0]);
            }
        }
        return toModelConfiguration(this.determineDefaultModel(this._copilotTokenObs.get(), this._defaultModelConfigObs.get()));
    }
    isConfiguredModel(model) {
        switch (model.source) {
            case "localConfig" /* ModelSource.LocalConfig */:
            case "expConfig" /* ModelSource.ExpConfig */:
            case "expDefaultConfig" /* ModelSource.ExpDefaultConfig */:
                return true;
            case "fetched" /* ModelSource.Fetched */:
            case "hardCodedDefault" /* ModelSource.HardCodedDefault */:
                return false;
            default:
                (0, assert_1.assertNever)(model.source);
        }
    }
    determineDefaultModel(copilotToken, defaultModelConfigString) {
        // if a default model config string is specified, use that
        if (defaultModelConfigString) {
            const parsedConfig = this.parseModelConfigString(defaultModelConfigString, configurationService_1.ConfigKey.TeamInternal.InlineEditsXtabProviderDefaultModelConfigurationString);
            if (parsedConfig) {
                return { ...parsedConfig, source: "expDefaultConfig" /* ModelSource.ExpDefaultConfig */ };
            }
        }
        // otherwise, use built-in defaults
        if (copilotToken?.isFcv1()) {
            return InlineEditsModelService_1.COPILOT_NES_XTAB_MODEL;
        }
        else if (copilotToken?.isFreeUser || copilotToken?.isNoAuthUser) {
            return InlineEditsModelService_1.COPILOT_NES_CALLISTO;
        }
        else {
            return InlineEditsModelService_1.COPILOT_NES_OCT;
        }
    }
    _pickModel({ preferredModelName, models, undesiredModelsManager, }) {
        // priority of picking a model:
        // 0. model from modelConfigurationString setting from ExP, unless marked as undesired
        // 1. user preferred model
        // 2. first model in the list
        const expConfiguredModel = models.find(m => m.source === "expConfig" /* ModelSource.ExpConfig */);
        if (expConfiguredModel) {
            const isUndesiredModelId = undesiredModelsManager.isUndesiredModelId(expConfiguredModel.modelName);
            if (isUndesiredModelId) {
                this._logger.trace(`Exp-configured model ${expConfiguredModel.modelName} is marked as undesired by the user. Skipping.`);
            }
            else {
                return expConfiguredModel;
            }
        }
        const userHasPreferredModel = preferredModelName !== 'none';
        if (userHasPreferredModel) {
            const preferredModel = models.find(m => m.modelName === preferredModelName);
            if (preferredModel) {
                return preferredModel;
            }
        }
        (0, assert_1.softAssert)(models.length > 0, 'InlineEdits model list should have at least one model');
        const model = models.at(0);
        if (model) {
            return model;
        }
        return this.determineDefaultModel(this._copilotTokenObs.get(), this._defaultModelConfigObs.get());
    }
    parseModelConfigString(configString, configKey) {
        let errorMessage;
        try {
            const parsed = JSON.parse(configString);
            const result = xtabPromptOptions_1.MODEL_CONFIGURATION_VALIDATOR.validate(parsed);
            if (!result.error) {
                return result.content;
            }
            errorMessage = result.error.message;
        }
        catch (e) {
            errorMessage = errors_1.ErrorUtils.toString(errors_1.ErrorUtils.fromUnknown(e));
        }
        /* __GDPR__
            "incorrectNesModelConfig" : {
                "owner": "ulugbekna",
                "comment": "Capture if model configuration string is invalid or malformed.",
                "configName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Name of the configuration that failed to parse." },
                "errorMessage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Error message from parsing or validation." },
                "configValue": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The invalid config string." }
            }
        */
        this._telemetryService.sendMSFTTelemetryEvent('incorrectNesModelConfig', { configName: configKey.id, errorMessage, configValue: configString });
        return undefined;
    }
};
exports.InlineEditsModelService = InlineEditsModelService;
exports.InlineEditsModelService = InlineEditsModelService = InlineEditsModelService_1 = __decorate([
    __param(0, copilotTokenStore_1.ICopilotTokenStore),
    __param(1, proxyModelsService_1.IProxyModelsService),
    __param(2, inlineEditsModelService_1.IUndesiredModelsManager),
    __param(3, configurationService_1.IConfigurationService),
    __param(4, nullExperimentationService_1.IExperimentationService),
    __param(5, telemetry_1.ITelemetryService),
    __param(6, logService_1.ILogService)
], InlineEditsModelService);
function toModelConfiguration(model) {
    const { source: _, ...config } = model;
    return config;
}
var UndesiredModels;
(function (UndesiredModels) {
    const UNDESIRED_MODELS_KEY = 'copilot.chat.nextEdits.undesiredModelIds';
    let Manager = class Manager extends lifecycle_1.Disposable {
        constructor(_vscodeExtensionContext) {
            super();
            this._vscodeExtensionContext = _vscodeExtensionContext;
            this._onDidChange = this._register(new event_1.Emitter());
            this.onDidChange = this._onDidChange.event;
            this._queue = new async_1.TaskQueue();
        }
        isUndesiredModelId(modelId) {
            const models = this._getModels();
            return models.includes(modelId);
        }
        addUndesiredModelId(modelId) {
            return this._queue.schedule(async () => {
                const models = this._getModels();
                if (!models.includes(modelId)) {
                    models.push(modelId);
                    await this._setModels(models);
                    this._onDidChange.fire();
                }
            });
        }
        removeUndesiredModelId(modelId) {
            return this._queue.schedule(async () => {
                const models = this._getModels();
                const index = models.indexOf(modelId);
                if (index !== -1) {
                    models.splice(index, 1);
                    await this._setModels(models);
                    this._onDidChange.fire();
                }
            });
        }
        _getModels() {
            return this._vscodeExtensionContext.globalState.get(UNDESIRED_MODELS_KEY) ?? [];
        }
        _setModels(models) {
            return new Promise((resolve, reject) => {
                this._vscodeExtensionContext.globalState.update(UNDESIRED_MODELS_KEY, models).then(resolve, reject);
            });
        }
    };
    Manager = __decorate([
        __param(0, extensionContext_1.IVSCodeExtensionContext)
    ], Manager);
    UndesiredModels.Manager = Manager;
})(UndesiredModels || (exports.UndesiredModels = UndesiredModels = {}));
//# sourceMappingURL=inlineEditsModelService.js.map