"use strict";
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 __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var OAuthCallbackCoordinatorService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.OAuthCallbackCoordinatorService = void 0;
const api_prisma_1 = require("@hed-hog/api-prisma");
const common_1 = require("@nestjs/common");
const crypto_1 = require("crypto");
const oauth_errors_1 = require("./oauth.errors");
let OAuthCallbackCoordinatorService = OAuthCallbackCoordinatorService_1 = class OAuthCallbackCoordinatorService {
    constructor(prisma) {
        this.prisma = prisma;
        this.logger = new common_1.Logger(OAuthCallbackCoordinatorService_1.name);
        this.completedCallbacks = new Map();
        this.completedTtlMs = 10 * 60 * 1000;
    }
    async execute(callbackKey, callback) {
        this.pruneCompletedCallbacks();
        if (this.isCompleted(callbackKey)) {
            throw new oauth_errors_1.OAuthCallbackAlreadyProcessedError();
        }
        try {
            if (this.prisma.isPostgres()) {
                return await this.executeWithPostgresLock(callbackKey, callback);
            }
            if (this.prisma.isMysql()) {
                return await this.executeWithMysqlLock(callbackKey, callback);
            }
            this.logger.warn(`OAuth callback coordination is running without distributed lock support for provider ${this.prisma.getProvider()}.`);
            const result = await callback();
            this.markCompleted(callbackKey);
            return result;
        }
        catch (error) {
            if (this.shouldMarkCompleted(error)) {
                this.markCompleted(callbackKey);
            }
            throw error;
        }
    }
    async executeWithPostgresLock(callbackKey, callback) {
        const lockKey = this.toSignedBigInt(callbackKey);
        return this.prisma.$transaction(async (tx) => {
            var _a;
            const rows = await tx.$queryRaw `
          SELECT pg_try_advisory_xact_lock(${lockKey}) AS locked
        `;
            if (!((_a = rows[0]) === null || _a === void 0 ? void 0 : _a.locked)) {
                throw new oauth_errors_1.OAuthCallbackInProgressError();
            }
            const result = await callback();
            this.markCompleted(callbackKey);
            return result;
        }, {
            maxWait: 5000,
            timeout: 60000,
        });
    }
    async executeWithMysqlLock(callbackKey, callback) {
        const lockName = this.toMysqlLockName(callbackKey);
        return this.prisma.$transaction(async (tx) => {
            var _a;
            const rows = await tx.$queryRaw(api_prisma_1.Prisma.sql `
          SELECT GET_LOCK(${lockName}, 0) AS locked
        `);
            if (((_a = rows[0]) === null || _a === void 0 ? void 0 : _a.locked) !== 1) {
                throw new oauth_errors_1.OAuthCallbackInProgressError();
            }
            try {
                const result = await callback();
                this.markCompleted(callbackKey);
                return result;
            }
            finally {
                await tx.$queryRaw(api_prisma_1.Prisma.sql `
            SELECT RELEASE_LOCK(${lockName}) AS released
          `);
            }
        }, {
            maxWait: 5000,
            timeout: 60000,
        });
    }
    shouldMarkCompleted(error) {
        return (error instanceof oauth_errors_1.OAuthCallbackAlreadyProcessedError ||
            (error instanceof oauth_errors_1.OAuthProviderException &&
                error.reason === 'callback_consumed'));
    }
    isCompleted(callbackKey) {
        const expiresAt = this.completedCallbacks.get(callbackKey);
        if (!expiresAt) {
            return false;
        }
        if (expiresAt <= Date.now()) {
            this.completedCallbacks.delete(callbackKey);
            return false;
        }
        return true;
    }
    markCompleted(callbackKey) {
        this.completedCallbacks.set(callbackKey, Date.now() + this.completedTtlMs);
    }
    pruneCompletedCallbacks() {
        const now = Date.now();
        for (const [callbackKey, expiresAt] of this.completedCallbacks.entries()) {
            if (expiresAt <= now) {
                this.completedCallbacks.delete(callbackKey);
            }
        }
    }
    toSignedBigInt(callbackKey) {
        const hex = (0, crypto_1.createHash)('sha256').update(callbackKey).digest('hex').slice(0, 16);
        const unsigned = BigInt(`0x${hex}`);
        const signedBoundary = BigInt(1) << BigInt(63);
        return unsigned >= signedBoundary
            ? unsigned - (BigInt(1) << BigInt(64))
            : unsigned;
    }
    toMysqlLockName(callbackKey) {
        const suffix = (0, crypto_1.createHash)('sha256').update(callbackKey).digest('hex').slice(0, 48);
        return `oauth:${suffix}`;
    }
};
exports.OAuthCallbackCoordinatorService = OAuthCallbackCoordinatorService;
exports.OAuthCallbackCoordinatorService = OAuthCallbackCoordinatorService = OAuthCallbackCoordinatorService_1 = __decorate([
    (0, common_1.Injectable)(),
    __metadata("design:paramtypes", [api_prisma_1.PrismaService])
], OAuthCallbackCoordinatorService);
//# sourceMappingURL=oauth-callback-coordinator.service.js.map