"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DeviceTracker = void 0;
const LogService_1 = require("../logging/LogService");
const Crypto_1 = require("../models/Crypto");
/**
 * Tracks user devices for encryption operations.
 * @category Encryption
 */
class DeviceTracker {
    constructor(client) {
        this.client = client;
        this.deviceListUpdates = {};
    }
    /**
     * Gets the device lists for the given user IDs. Outdated device lists will be updated before
     * returning.
     * @param {string[]} userIds The user IDs to get the device lists of.
     * @returns {Promise<Record<string, UserDevice[]>>} Resolves to a map of user ID to device list.
     * If a user has no devices, they may be excluded from the result or appear as an empty array.
     */
    getDevicesFor(userIds) {
        return __awaiter(this, void 0, void 0, function* () {
            const outdatedUserIds = [];
            for (const userId of userIds) {
                const isOutdated = yield this.client.cryptoStore.isUserOutdated(userId);
                if (isOutdated)
                    outdatedUserIds.push(userId);
            }
            yield this.updateUsersDeviceLists(outdatedUserIds);
            const userDeviceMap = {};
            for (const userId of userIds) {
                userDeviceMap[userId] = yield this.client.cryptoStore.getActiveUserDevices(userId);
            }
            return userDeviceMap;
        });
    }
    /**
     * Flags multiple user's device lists as outdated, optionally queuing an immediate update.
     * @param {string} userIds The user IDs to flag the device lists of.
     * @param {boolean} resync True (default) to queue an immediate update, false otherwise.
     * @returns {Promise<void>} Resolves when the flagging has completed. Will wait for the resync
     * if requested too.
     */
    flagUsersOutdated(userIds, resync = true) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.client.cryptoStore.flagUsersOutdated(userIds);
            if (resync) {
                yield this.updateUsersDeviceLists(userIds);
            }
        });
    }
    /**
     * Updates multiple user's device lists regardless of outdated flag.
     * @param {string[]} userIds The user IDs to update.
     * @returns {Promise<void>} Resolves when complete.
     */
    updateUsersDeviceLists(userIds) {
        return __awaiter(this, void 0, void 0, function* () {
            // We wait for the lock, but still run through with our update just in case we are lagged.
            // This can happen if the server is slow to reply to device list queries, but a user is
            // changing information about their device a lot.
            const existingPromises = userIds.map(u => this.deviceListUpdates[u]).filter(p => !!p);
            if (existingPromises.length > 0) {
                yield Promise.all(existingPromises);
            }
            const promise = new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
                var _a, _b;
                try {
                    const resp = yield this.client.getUserDevices(userIds);
                    for (const userId of Object.keys(resp.device_keys)) {
                        if (!userIds.includes(userId)) {
                            LogService_1.LogService.warn("DeviceTracker", `Server returned unexpected user ID: ${userId} - ignoring user`);
                            continue;
                        }
                        const validated = [];
                        for (const deviceId of Object.keys(resp.device_keys[userId])) {
                            const device = resp.device_keys[userId][deviceId];
                            if (device.user_id !== userId || device.device_id !== deviceId) {
                                LogService_1.LogService.warn("DeviceTracker", `Server appears to be lying about device lists: ${userId} ${deviceId} has unexpected device ${device.user_id} ${device.device_id} listed - ignoring device`);
                                continue;
                            }
                            const ed25519 = device.keys[`${Crypto_1.DeviceKeyAlgorithm.Ed25519}:${deviceId}`];
                            const curve25519 = device.keys[`${Crypto_1.DeviceKeyAlgorithm.Curve25519}:${deviceId}`];
                            if (!ed25519 || !curve25519) {
                                LogService_1.LogService.warn("DeviceTracker", `Device ${userId} ${deviceId} is missing either an Ed25519 or Curve25519 key - ignoring device`);
                                continue;
                            }
                            const currentDevices = yield this.client.cryptoStore.getAllUserDevices(userId);
                            const existingDevice = currentDevices.find(d => d.device_id === deviceId);
                            if (existingDevice) {
                                const existingEd25519 = existingDevice.keys[`${Crypto_1.DeviceKeyAlgorithm.Ed25519}:${deviceId}`];
                                if (existingEd25519 !== ed25519) {
                                    LogService_1.LogService.warn("DeviceTracker", `Device ${userId} ${deviceId} appears compromised: Ed25519 key changed - ignoring device`);
                                    continue;
                                }
                            }
                            const signature = (_b = (_a = device.signatures) === null || _a === void 0 ? void 0 : _a[userId]) === null || _b === void 0 ? void 0 : _b[`${Crypto_1.DeviceKeyAlgorithm.Ed25519}:${deviceId}`];
                            if (!signature) {
                                LogService_1.LogService.warn("DeviceTracker", `Device ${userId} ${deviceId} is missing a signature - ignoring device`);
                                continue;
                            }
                            const validSignature = yield this.client.crypto.verifySignature(device, ed25519, signature);
                            if (!validSignature) {
                                LogService_1.LogService.warn("DeviceTracker", `Device ${userId} ${deviceId} has an invalid signature - ignoring device`);
                                continue;
                            }
                            validated.push(device);
                        }
                        yield this.client.cryptoStore.setActiveUserDevices(userId, validated);
                    }
                }
                catch (e) {
                    LogService_1.LogService.error("DeviceTracker", "Error updating device lists:", e);
                    // return reject(e);
                }
                resolve();
            }));
            userIds.forEach(u => this.deviceListUpdates[u] = promise);
            yield promise;
        });
    }
}
exports.DeviceTracker = DeviceTracker;
