"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TestimAuthApi = void 0;
const node_fetch_1 = __importStar(require("node-fetch"));
const jsonwebtoken_1 = require("jsonwebtoken");
const apiErrors_1 = require("./apiErrors");
const FIVE_MIN_IN_MS = 5 * 60 * 1000;
class TestimAuthApi {
    constructor(baseUrl = 'https://services.testim.io', fetch = node_fetch_1.default) {
        this.baseUrl = baseUrl;
        this.fetch = fetch;
        this.getToken = ensureOnlyOneRequestAtATime(this.getToken.bind(this));
    }
    async getToken(projectId, ciToken, signal) {
        return this.obtainTokenFromCiTokenIfNeeded(projectId, ciToken, signal);
    }
    async isAuthenticated(signal) {
        var _a;
        const headers = new node_fetch_1.Headers();
        this.addAuthorizationHeader(headers);
        const alive = await this.fetch(`${this.baseUrl}/auth/check/${(_a = this.tokenState) === null || _a === void 0 ? void 0 : _a.projectId}`, {
            headers: headers,
            signal: signal,
        });
        return alive.ok;
    }
    addAuthorizationHeader(headers) {
        var _a, _b;
        if (!((_a = this.tokenState) === null || _a === void 0 ? void 0 : _a.serverToken) || !headers) {
            throw new TypeError('Attempted to add authorizaiton header without authorizing first or without headers');
        }
        headers.set('Authorization', `Bearer ${(_b = this.tokenState) === null || _b === void 0 ? void 0 : _b.serverToken}`);
    }
    async obtainTokenFromCiTokenIfNeeded(projectId, token, signal) {
        var _a, _b, _c;
        if (!((_a = this.tokenState) === null || _a === void 0 ? void 0 : _a.serverTokenExpiryMilliseconds)) {
            return this.obtainTokenFromCiToken(projectId, token, signal);
        }
        if (token !== ((_b = this.tokenState) === null || _b === void 0 ? void 0 : _b.projectId)) {
            return this.obtainTokenFromCiToken(projectId, token, signal);
        }
        const hasTokenExpired = ((_c = this.tokenState) === null || _c === void 0 ? void 0 : _c.serverTokenExpiryMilliseconds) > Date.now() + FIVE_MIN_IN_MS;
        if (hasTokenExpired) {
            try {
                return this.refreshTokenFromObtainedToken(signal);
            }
            catch (e) {
                return this.obtainTokenFromCiToken(projectId, token, signal);
            }
        }
        return this.tokenState.serverToken;
    }
    async refreshTokenFromObtainedToken(signal) {
        var _a, _b;
        if (!this.tokenState) {
            throw new TypeError('Error, attempted to refresh a non-existing token');
        }
        const response = await this.fetch(`${this.baseUrl}/auth/refreshToken`, {
            method: 'POST',
            body: JSON.stringify({
                token: (_a = this.tokenState) === null || _a === void 0 ? void 0 : _a.serverToken,
                refreshToken: (_b = this.tokenState) === null || _b === void 0 ? void 0 : _b.refreshToken,
            }),
            headers: {
                'content-type': 'application/json',
            },
            signal: signal,
        });
        this.tokenState.serverToken = response.token;
        this.tokenState.serverTokenExpiryMilliseconds = jsonwebtoken_1.decode(response.token).exp * 1000;
        return this.tokenState.serverToken;
    }
    async obtainTokenFromCiToken(projectId, token, signal) {
        let customTokenResponse;
        const throwUnauthorizedError = (e) => {
            throw new apiErrors_1.TestimApiError(e, `Error trying to retrieve CLI token. Your CLI token and project might not match. `, 'E_CREDENTIALS_DONT_MATCH');
        };
        try {
            customTokenResponse = await this.fetch(`${this.baseUrl}/auth/token`, {
                method: 'POST',
                headers: {
                    'content-type': 'application/json',
                },
                signal: signal,
                body: JSON.stringify({ projectId, token }),
            }).then((x) => x.json());
        }
        catch (e) {
            if (e.message.includes('Unauthorized')) {
                throwUnauthorizedError(e);
            }
            throw new apiErrors_1.TestimApiError(e, e.message, 'E_UNAUTHENTICATED');
        }
        if ((customTokenResponse === null || customTokenResponse === void 0 ? void 0 : customTokenResponse.status) === 'error') {
            if ((customTokenResponse === null || customTokenResponse === void 0 ? void 0 : customTokenResponse.code) === 'unauthorized') {
                throwUnauthorizedError(customTokenResponse.code);
            }
        }
        const serverToken = customTokenResponse.token;
        this.tokenState = {
            serverToken,
            serverTokenExpiryMilliseconds: jsonwebtoken_1.decode(serverToken).exp * 1000,
            refreshToken: customTokenResponse.refreshToken,
            ngrokTunnelToken: customTokenResponse.ngrokToken,
            projectId,
        };
        return this.tokenState.serverToken;
    }
}
exports.TestimAuthApi = TestimAuthApi;
function ensureOnlyOneRequestAtATime(fn) {
    let inFlight = null;
    return function (...args) {
        if (inFlight) {
            return inFlight;
        }
        inFlight = fn.call(this, ...args);
        Promise.resolve(inFlight).finally(() => {
            inFlight = null;
        });
        return inFlight;
    };
}
