"use strict";
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitHubAuthenticationProvider = exports.AuthProviderType = void 0;
const vscode = require("vscode");
const uuid_1 = require("uuid");
const keychain_1 = require("./common/keychain");
const githubServer_1 = require("./githubServer");
const utils_1 = require("./common/utils");
const experimentationService_1 = require("./experimentationService");
const vscode_extension_telemetry_1 = require("vscode-extension-telemetry");
const logger_1 = require("./common/logger");
var AuthProviderType;
(function (AuthProviderType) {
    AuthProviderType["github"] = "github";
    AuthProviderType["githubEnterprise"] = "github-enterprise";
})(AuthProviderType = exports.AuthProviderType || (exports.AuthProviderType = {}));
class GitHubAuthenticationProvider {
    constructor(context, type) {
        this.context = context;
        this.type = type;
        this._sessionChangeEmitter = new vscode.EventEmitter();
        this._logger = new logger_1.Log(this.type);
        this._keychain = new keychain_1.Keychain(this.context, `${this.type}.auth`, this._logger);
        const { name, version, aiKey } = context.extension.packageJSON;
        this._telemetryReporter = new experimentationService_1.ExperimentationTelemetry(context, new vscode_extension_telemetry_1.default(name, version, aiKey));
        if (this.type === AuthProviderType.github) {
            this._githubServer = new githubServer_1.GitHubServer(this._logger, this._telemetryReporter);
        }
        else {
            this._githubServer = new githubServer_1.GitHubEnterpriseServer(this._logger, this._telemetryReporter);
        }
        // Contains the current state of the sessions we have available.
        this._sessionsPromise = this.readSessions();
        this._disposable = vscode.Disposable.from(this._telemetryReporter, this._githubServer, vscode.authentication.registerAuthenticationProvider(type, this._githubServer.friendlyName, this, { supportsMultipleAccounts: false }), this.context.secrets.onDidChange(() => this.checkForUpdates()));
    }
    dispose() {
        this._disposable.dispose();
    }
    get onDidChangeSessions() {
        return this._sessionChangeEmitter.event;
    }
    async getSessions(scopes) {
        this._logger.info(`Getting sessions for ${scopes?.join(',') || 'all scopes'}...`);
        const sessions = await this._sessionsPromise;
        const finalSessions = scopes
            ? sessions.filter(session => (0, utils_1.arrayEquals)([...session.scopes].sort(), scopes.sort()))
            : sessions;
        this._logger.info(`Got ${finalSessions.length} sessions for ${scopes?.join(',') || 'all scopes'}...`);
        return finalSessions;
    }
    async afterTokenLoad(token) {
        this._githubServer.sendAdditionalTelemetryInfo(token);
    }
    async checkForUpdates() {
        const previousSessions = await this._sessionsPromise;
        this._sessionsPromise = this.readSessions();
        const storedSessions = await this._sessionsPromise;
        const added = [];
        const removed = [];
        storedSessions.forEach(session => {
            const matchesExisting = previousSessions.some(s => s.id === session.id);
            // Another window added a session to the keychain, add it to our state as well
            if (!matchesExisting) {
                this._logger.info('Adding session found in keychain');
                added.push(session);
            }
        });
        previousSessions.forEach(session => {
            const matchesExisting = storedSessions.some(s => s.id === session.id);
            // Another window has logged out, remove from our state
            if (!matchesExisting) {
                this._logger.info('Removing session no longer found in keychain');
                removed.push(session);
            }
        });
        if (added.length || removed.length) {
            this._sessionChangeEmitter.fire({ added, removed, changed: [] });
        }
    }
    async readSessions() {
        let sessionData;
        try {
            this._logger.info('Reading sessions from keychain...');
            const storedSessions = await this._keychain.getToken() || await this._keychain.tryMigrate();
            if (!storedSessions) {
                return [];
            }
            this._logger.info('Got stored sessions!');
            try {
                sessionData = JSON.parse(storedSessions);
            }
            catch (e) {
                await this._keychain.deleteToken();
                throw e;
            }
        }
        catch (e) {
            this._logger.error(`Error reading token: ${e}`);
            return [];
        }
        const sessionPromises = sessionData.map(async (session) => {
            let userInfo;
            if (!session.account) {
                try {
                    userInfo = await this._githubServer.getUserInfo(session.accessToken);
                    this._logger.info(`Verified session with the following scopes: ${session.scopes}`);
                }
                catch (e) {
                    // Remove sessions that return unauthorized response
                    if (e.message === 'Unauthorized') {
                        return undefined;
                    }
                }
            }
            setTimeout(() => this.afterTokenLoad(session.accessToken), 1000);
            this._logger.trace(`Read the following session from the keychain with the following scopes: ${session.scopes}`);
            return {
                id: session.id,
                account: {
                    label: session.account
                        ? session.account.label ?? session.account.displayName ?? '<unknown>'
                        : userInfo?.accountName ?? '<unknown>',
                    id: session.account?.id ?? userInfo?.id ?? '<unknown>'
                },
                scopes: session.scopes,
                accessToken: session.accessToken
            };
        });
        const verifiedSessions = (await Promise.allSettled(sessionPromises))
            .filter(p => p.status === 'fulfilled')
            .map(p => p.value)
            .filter((p) => Boolean(p));
        this._logger.info(`Got ${verifiedSessions.length} verified sessions.`);
        if (verifiedSessions.length !== sessionData.length) {
            await this.storeSessions(verifiedSessions);
        }
        return verifiedSessions;
    }
    async storeSessions(sessions) {
        this._logger.info(`Storing ${sessions.length} sessions...`);
        this._sessionsPromise = Promise.resolve(sessions);
        await this._keychain.setToken(JSON.stringify(sessions));
        this._logger.info(`Stored ${sessions.length} sessions!`);
    }
    async createSession(scopes) {
        try {
            /* __GDPR__
                "login" : {
                    "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
                }
            */
            this._telemetryReporter?.sendTelemetryEvent('login', {
                scopes: JSON.stringify(scopes),
            });
            const token = await this._githubServer.login(scopes.join(' '));
            this.afterTokenLoad(token);
            const session = await this.tokenToSession(token, scopes);
            const sessions = await this._sessionsPromise;
            const sessionIndex = sessions.findIndex(s => s.id === session.id);
            if (sessionIndex > -1) {
                sessions.splice(sessionIndex, 1, session);
            }
            else {
                sessions.push(session);
            }
            await this.storeSessions(sessions);
            this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] });
            this._logger.info('Login success!');
            return session;
        }
        catch (e) {
            // If login was cancelled, do not notify user.
            if (e === 'Cancelled') {
                /* __GDPR__
                    "loginCancelled" : { }
                */
                this._telemetryReporter?.sendTelemetryEvent('loginCancelled');
                throw e;
            }
            /* __GDPR__
                "loginFailed" : { }
            */
            this._telemetryReporter?.sendTelemetryEvent('loginFailed');
            vscode.window.showErrorMessage(`Sign in failed: ${e}`);
            this._logger.error(e);
            throw e;
        }
    }
    async tokenToSession(token, scopes) {
        const userInfo = await this._githubServer.getUserInfo(token);
        return {
            id: (0, uuid_1.v4)(),
            accessToken: token,
            account: { label: userInfo.accountName, id: userInfo.id },
            scopes
        };
    }
    async removeSession(id) {
        try {
            /* __GDPR__
                "logout" : { }
            */
            this._telemetryReporter?.sendTelemetryEvent('logout');
            this._logger.info(`Logging out of ${id}`);
            const sessions = await this._sessionsPromise;
            const sessionIndex = sessions.findIndex(session => session.id === id);
            if (sessionIndex > -1) {
                const session = sessions[sessionIndex];
                sessions.splice(sessionIndex, 1);
                await this.storeSessions(sessions);
                this._sessionChangeEmitter.fire({ added: [], removed: [session], changed: [] });
            }
            else {
                this._logger.error('Session not found');
            }
        }
        catch (e) {
            /* __GDPR__
                "logoutFailed" : { }
            */
            this._telemetryReporter?.sendTelemetryEvent('logoutFailed');
            vscode.window.showErrorMessage(`Sign out failed: ${e}`);
            this._logger.error(e);
            throw e;
        }
    }
}
exports.GitHubAuthenticationProvider = GitHubAuthenticationProvider;
//# sourceMappingURL=github.js.map