"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); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserInteractionMonitor = exports.MAX_INTERACTIONS_STORED = exports.MAX_INTERACTIONS_CONSIDERED = exports.ActionKind = void 0;
exports.getWindowWithIgnoredLimit = getWindowWithIgnoredLimit;
exports.getUserHappinessScore = getUserHappinessScore;
const configurationService_1 = require("../../../platform/configuration/common/configurationService");
const xtabPromptOptions_1 = require("../../../platform/inlineEdits/common/dataTypes/xtabPromptOptions");
const logService_1 = require("../../../platform/log/common/logService");
const nullExperimentationService_1 = require("../../../platform/telemetry/common/nullExperimentationService");
const telemetry_1 = require("../../../platform/telemetry/common/telemetry");
const errors_1 = require("../../../util/common/errors");
const delay_1 = require("./delay");
var ActionKind;
(function (ActionKind) {
    ActionKind["Accepted"] = "accepted";
    ActionKind["Rejected"] = "rejected";
    ActionKind["Ignored"] = "ignored";
})(ActionKind || (exports.ActionKind = ActionKind = {}));
exports.MAX_INTERACTIONS_CONSIDERED = 10;
exports.MAX_INTERACTIONS_STORED = 30;
/**
 * Get window of actions with ignored action limiting via window expansion.
 *
 * When ignored limit is reached, skip excess ignored actions but expand window
 * further back to still get MAX_INTERACTIONS_CONSIDERED items.
 */
function getWindowWithIgnoredLimit(actions, config) {
    const { limitConsecutiveIgnored, limitTotalIgnored, ignoredLimit } = config;
    if (!limitConsecutiveIgnored && !limitTotalIgnored) {
        // No limiting - just take last MAX_INTERACTIONS_CONSIDERED
        return actions.slice(-exports.MAX_INTERACTIONS_CONSIDERED);
    }
    const result = [];
    let consecutiveIgnored = 0;
    let totalIgnored = 0;
    // Walk backwards through history
    for (let i = actions.length - 1; i >= 0 && result.length < exports.MAX_INTERACTIONS_CONSIDERED; i--) {
        const action = actions[i];
        if (action.kind === ActionKind.Ignored) {
            let skip = false;
            if (limitConsecutiveIgnored && consecutiveIgnored >= ignoredLimit) {
                skip = true;
            }
            if (limitTotalIgnored && totalIgnored >= ignoredLimit) {
                skip = true;
            }
            if (skip) {
                continue;
            }
            consecutiveIgnored++;
            totalIgnored++;
        }
        else {
            consecutiveIgnored = 0; // Reset consecutive count on accept/reject
        }
        result.push(action);
    }
    // Reverse to get chronological order
    result.reverse();
    return result;
}
/**
 * Calculate user happiness score from actions.
 * Value between 0 and 1 indicating user happiness.
 * 1 means very happy, 0 means very unhappy.
 *
 * Uses position-weighted scoring with ignored action limiting:
 * - More recent actions have higher weight
 * - Ignored actions can be limited (consecutive or total) to prevent score dilution
 * - Score is adjusted towards neutral (0.5) based on data confidence
 */
function getUserHappinessScore(actions, config) {
    if (actions.length === 0) {
        return 0.5; // neutral score when no data
    }
    // Get window of actions with ignored limiting
    const window = getWindowWithIgnoredLimit(actions, config);
    if (window.length === 0) {
        return 0.5; // neutral score when no data after filtering
    }
    // Calculate weighted score
    let weightedScore = 0; // Sum of weighted normalized scores
    let totalWeight = 0; // Sum of weights applied
    let scoredActionCount = 0; // Count of actions that contributed to score
    for (let i = 0; i < window.length; i++) {
        const action = window[i];
        // Skip ignored actions if not included in score calculation
        if (action.kind === ActionKind.Ignored && !config.includeIgnored) {
            continue;
        }
        scoredActionCount++;
        // Calculate weight based on position (more recent = higher weight)
        // Position 0 (oldest) has lowest weight, last position has highest weight
        const weight = i + 1;
        // Get score based on action kind from configuration
        let score;
        switch (action.kind) {
            case ActionKind.Accepted:
                score = config.acceptedScore;
                break;
            case ActionKind.Rejected:
                score = config.rejectedScore;
                break;
            case ActionKind.Ignored:
                score = config.ignoredScore;
                break;
        }
        // Normalize score to 0-1 range based on accept/reject weights
        const normalized = (score - config.rejectedScore) / (config.acceptedScore - config.rejectedScore);
        weightedScore += normalized * weight;
        totalWeight += weight;
    }
    const rawScore = totalWeight > 0 ? weightedScore / totalWeight : 0.5;
    // Adjust score towards neutral (0.5) when we have fewer data points
    // This prevents extreme scores with limited data
    const dataConfidence = scoredActionCount / exports.MAX_INTERACTIONS_CONSIDERED;
    return 0.5 + (rawScore - 0.5) * dataConfidence;
}
let UserInteractionMonitor = class UserInteractionMonitor {
    constructor(_configurationService, _experimentationService, _logService, _telemetryService) {
        this._configurationService = _configurationService;
        this._experimentationService = _experimentationService;
        this._logService = _logService;
        this._telemetryService = _telemetryService;
        /**
         * Used for aggressiveness level calculation.
         * Includes all action types (accepted, rejected, ignored).
         */
        this._recentUserActionsForAggressiveness = [];
        /**
         * Used for timing/debounce calculation.
         * Only includes accepted and rejected actions (ignored actions don't affect timing).
         */
        this._recentUserActionsForTiming = [];
        this._lastActionWasAcceptance = false;
    }
    // Capture user interactions
    handleAcceptance() {
        this._recordUserAction(ActionKind.Accepted);
    }
    handleRejection() {
        this._recordUserAction(ActionKind.Rejected);
    }
    handleIgnored() {
        this._recordUserAction(ActionKind.Ignored);
    }
    /**
     * Returns true if the last recorded user action was an acceptance.
     * Used to skip aggressiveness min-response-time delay after accepts.
     */
    get wasLastActionAcceptance() {
        return this._lastActionWasAcceptance;
    }
    _recordUserAction(kind) {
        const now = Date.now();
        this._lastActionWasAcceptance = kind === ActionKind.Accepted;
        // Always record for aggressiveness calculation
        this._recentUserActionsForAggressiveness.push({ time: now, kind });
        this._recentUserActionsForAggressiveness = this._recentUserActionsForAggressiveness.slice(-exports.MAX_INTERACTIONS_STORED);
        // Only record accepts/rejects for timing calculation
        if (kind !== ActionKind.Ignored) {
            this._recentUserActionsForTiming.push({ time: now, kind });
            this._recentUserActionsForTiming = this._recentUserActionsForTiming.slice(-exports.MAX_INTERACTIONS_CONSIDERED);
        }
    }
    // Creates a DelaySession based on recent user interactions
    createDelaySession(requestTime) {
        const baseDebounceTime = this._configurationService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsDebounce, this._experimentationService);
        const backoffDebounceEnabled = this._configurationService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsBackoffDebounceEnabled, this._experimentationService);
        const expectedTotalTime = backoffDebounceEnabled ? this._getExpectedTotalTime(baseDebounceTime) : undefined;
        return new delay_1.DelaySession(baseDebounceTime, expectedTotalTime, requestTime);
    }
    _getExpectedTotalTime(baseDebounceTime) {
        const DEBOUNCE_DECAY_TIME_MS = 10 * 60 * 1000; // 10 minutes
        const MAX_DEBOUNCE_TIME = 3000; // 3 seconds
        const MIN_DEBOUNCE_TIME = 50; // 50 ms
        const REJECTION_WEIGHT = 1.5;
        const ACCEPTANCE_WEIGHT = 0.8;
        const now = Date.now();
        let multiplier = 1;
        // Calculate impact of each action with time decay
        // Uses timing-specific array which only contains accepts/rejects
        for (const action of this._recentUserActionsForTiming) {
            const timeSinceAction = now - action.time;
            if (timeSinceAction > DEBOUNCE_DECAY_TIME_MS) {
                continue;
            }
            // Exponential decay: impact decreases as time passes
            const decayFactor = Math.exp(-timeSinceAction / DEBOUNCE_DECAY_TIME_MS);
            const actionWeight = action.kind === ActionKind.Rejected ? REJECTION_WEIGHT : ACCEPTANCE_WEIGHT;
            multiplier *= 1 + ((actionWeight - 1) * decayFactor);
        }
        let debounceTime = baseDebounceTime * multiplier;
        // Clamp the debounce time to reasonable bounds
        debounceTime = Math.min(MAX_DEBOUNCE_TIME, Math.max(MIN_DEBOUNCE_TIME, debounceTime));
        return debounceTime;
    }
    // Determine aggressiveness level based on user interactions
    /**
     * Returns the aggressiveness level and the user happiness score that was used to derive it.
     * The score is returned to avoid race conditions when logging telemetry.
     */
    getAggressivenessLevel() {
        // User-facing setting takes priority when explicitly set to a non-default value
        const userAggressiveness = this._configurationService.getExperimentBasedConfig(configurationService_1.ConfigKey.Advanced.InlineEditsAggressiveness, this._experimentationService);
        const userLevel = xtabPromptOptions_1.AggressivenessSetting.toLevel(userAggressiveness);
        if (userLevel !== undefined) {
            return { aggressivenessLevel: userLevel, userHappinessScore: undefined };
        }
        // Team-internal experiment-based override
        const configuredAggressivenessLevel = this._configurationService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsXtabAggressivenessLevel, this._experimentationService);
        if (configuredAggressivenessLevel !== undefined) {
            return { aggressivenessLevel: configuredAggressivenessLevel, userHappinessScore: undefined };
        }
        // Default or unrecognized: fall through to happiness-score-based logic
        let level;
        const config = this._getUserHappinessScoreConfiguration();
        const userHappinessScore = this._getUserHappinessScore(config);
        if (userHappinessScore >= config.highThreshold) {
            level = xtabPromptOptions_1.AggressivenessLevel.High;
        }
        else if (userHappinessScore >= config.mediumThreshold) {
            level = xtabPromptOptions_1.AggressivenessLevel.Medium;
        }
        else {
            level = xtabPromptOptions_1.AggressivenessLevel.Low;
        }
        return { aggressivenessLevel: level, userHappinessScore };
    }
    _getUserHappinessScoreConfiguration() {
        const configKey = configurationService_1.ConfigKey.TeamInternal.InlineEditsUserHappinessScoreConfigurationString;
        const configString = this._configurationService.getExperimentBasedConfig(configKey, this._experimentationService);
        if (configString === undefined) {
            return xtabPromptOptions_1.DEFAULT_USER_HAPPINESS_SCORE_CONFIGURATION;
        }
        try {
            return (0, xtabPromptOptions_1.parseUserHappinessScoreConfigurationString)(configString);
        }
        catch (e) {
            this._logService.error(e, 'Failed to parse user happiness score configuration, using default config');
            // Log to telemetry when we fail to parse an experimental config, but still offer the default config to avoid disruption.
            /* __GDPR__
                "incorrectNesAdaptiveAggressivenessConfig" : {
                    "owner": "bstee615",
                    "comment": "Capture if model configuration string is invalid JSON.",
                    "configName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Name of the configuration that failed to parse." },
                    "errorMessage": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Error message from JSON.parse." },
                    "configValue": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The invalid JSON string." }
                }
            */
            this._telemetryService.sendMSFTTelemetryEvent('incorrectNesAdaptiveAggressivenessConfig', { configName: configKey.id, errorMessage: errors_1.ErrorUtils.toString(errors_1.ErrorUtils.fromUnknown(e)), configValue: configString });
            return xtabPromptOptions_1.DEFAULT_USER_HAPPINESS_SCORE_CONFIGURATION;
        }
    }
    _getUserHappinessScore(config) {
        return getUserHappinessScore(this._recentUserActionsForAggressiveness, config);
    }
};
exports.UserInteractionMonitor = UserInteractionMonitor;
exports.UserInteractionMonitor = UserInteractionMonitor = __decorate([
    __param(0, configurationService_1.IConfigurationService),
    __param(1, nullExperimentationService_1.IExperimentationService),
    __param(2, logService_1.ILogService),
    __param(3, telemetry_1.ITelemetryService)
], UserInteractionMonitor);
//# sourceMappingURL=userInteractionMonitor.js.map