import { difference } from 'lodash';
import { DataTypes, Op, Sequelize, Transaction } from 'sequelize';
import { DEVICE_SCOPES, DEVICE_SCOPES_SUBJECT_TO_QUOTA, SETTING_KEYS, SYNC_DIRECTIONS } from '@tamanu/constants';
import { AuthPermissionError, InvalidOperationError, MissingCredentialError, QuotaExceededError } from '@tamanu/errors';
import { stringToStableInteger } from '@tamanu/shared/utils';
import { Model } from './Model';
const BASE_DEVICE_REGISTRATION_ADVISORY_KEY = 'deviceRegistrationAdvisoryLock';
export class Device extends Model {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    static initModel({ primaryKey, ...options }) {
        super.init({
            id: {
                type: DataTypes.TEXT,
                allowNull: false,
                primaryKey: true,
                defaultValue: Sequelize.fn('gen_random_uuid')
            },
            lastSeenAt: {
                type: DataTypes.DATE,
                allowNull: false,
                defaultValue: Sequelize.fn('now')
            },
            scopes: {
                type: DataTypes.JSONB,
                allowNull: false,
                defaultValue: []
            },
            name: {
                type: DataTypes.TEXT
            }
        }, {
            ...options,
            syncDirection: SYNC_DIRECTIONS.DO_NOT_SYNC,
            validate: {
                mustHaveKnownScopes () {
                    if (// `as` necessary because of incomplete typings in sequelize
                    this.scopes.some((scope)=>!Object.values(DEVICE_SCOPES).includes(scope))) {
                        throw new InvalidOperationError('Device must only use known scopes.');
                    }
                }
            }
        });
    }
    static initRelations(models) {
        this.belongsTo(models.User, {
            foreignKey: 'registeredById',
            as: 'registeredBy'
        });
    }
    static async getQuotaByUserId(userId) {
        return this.count({
            where: {
                registeredById: userId,
                [Op.or]: DEVICE_SCOPES_SUBJECT_TO_QUOTA.map((scope)=>// the jsonb operator `?`: "Does the string exist as a top-level key within the JSON value?"
                    Sequelize.literal(`scopes ? '${scope}'`))
            }
        });
    }
    async markSeen() {
        await this.update({
            lastSeenAt: Sequelize.fn('now')
        });
    }
    requiresQuota() {
        return this.scopes.some((scope)=>DEVICE_SCOPES_SUBJECT_TO_QUOTA.includes(scope));
    }
    ensureHasScope(scope) {
        if (!this.scopes.includes(scope)) {
            throw new Error('Device must have the required scope.');
        }
    }
    static async ensureRegistration({ settings, user, deviceId, scopes }) {
        // There's two race conditions we seek to avoid with this transaction:
        //
        // 1. If the device logs in to the server twice simultaneously, we want
        //    to avoid two SyncDevice records being created. This is also mitigated
        //    by the UNIQUE constraint on the table.
        //
        // 2. If two unknown devices try to register at the same time, and the
        //    quota is reached on the first one, the second should not be able to.
        return await Device.sequelize.transaction({
            isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE
        }, async ()=>{
            if (!deviceId) {
                if (scopes.includes(DEVICE_SCOPES.SYNC_CLIENT)) {
                    throw new MissingCredentialError('Device ID is required');
                } else {
                    return;
                }
            }
            // If the same user tries to register two devices concurrently, the second will wait until the first is complete.
            await Device.acquireRegistrationLockForUser(user.id);
            const syncDevice = await Device.findByPk(deviceId);
            if (syncDevice) {
                if (difference(scopes, syncDevice.scopes).length > 0) {
                    throw new AuthPermissionError('Requested more scopes than the device has');
                }
                await syncDevice.markSeen();
                return syncDevice;
            }
            const device = new Device({
                id: deviceId,
                registeredById: user.id,
                scopes
            });
            const deviceRegistrationQuotaEnabled = await settings.get(SETTING_KEYS.FEATURES_DEVICE_REGISTRATION_QUOTA_ENABLED);
            if (deviceRegistrationQuotaEnabled && device.requiresQuota()) {
                const currentCount = await Device.getQuotaByUserId(user.id);
                if (currentCount + 1 > user.deviceRegistrationQuota) {
                    throw new QuotaExceededError();
                }
            }
            await device.save();
            return device;
        });
    }
    static async acquireRegistrationLockForUser(userId) {
        const lockId = stringToStableInteger(`${BASE_DEVICE_REGISTRATION_ADVISORY_KEY}:${userId}`);
        await Device.sequelize.query(`SELECT pg_advisory_xact_lock(:lockId)`, {
            replacements: {
                lockId
            }
        });
    }
}

//# sourceMappingURL=Device.js.map