"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TransactionNotificationService = void 0;
const transaction_data_1 = require("../../domain/value-objects/transaction-data");
const MAX_NOTIFICATION_DECIMALS = 10;
/**
 * Formats a raw integer token value using the token's decimals.
 *
 * The result is trimmed to a maximum number of decimal places so long
 * fractional amounts do not overflow the notification title.
 */
const formatTokenValue = (value, decimals) => {
    if (decimals <= 0) {
        return value;
    }
    const bigValue = BigInt(value);
    const divisor = 10n ** BigInt(decimals);
    const integerPart = bigValue / divisor;
    const fractionalPart = bigValue % divisor;
    const fractionalStr = fractionalPart.toString().padStart(decimals, '0');
    const limited = fractionalStr.slice(0, MAX_NOTIFICATION_DECIMALS);
    const trimmed = limited.replace(/0+$/, '');
    if (trimmed.length === 0) {
        return integerPart.toString();
    }
    return `${integerPart}.${trimmed}`;
};
/**
 * Listens to transaction lifecycle events and notifies the active devices
 * linked to the wallets that own the addresses involved in the transaction.
 *
 * Business rules enforced here:
 *   - Only addresses that are actually linked to a wallet are considered.
 *   - Only devices linked to those wallets are considered.
 *   - Only devices whose status is 'active' are notified via WebSocket.
 *   - Push notifications are sent to devices whose status is 'active' or
 *     'background'.
 *   - The gateway decides whether the device is currently connected; this
 *     service only asks the gateway to deliver the message.
 */
class TransactionNotificationService {
    eventPublisher;
    addressRepository;
    walletRepository;
    deviceRepository;
    tokenRepository;
    networkRepository;
    notificationGateway;
    pushGateway;
    constructor(eventPublisher, addressRepository, walletRepository, deviceRepository, tokenRepository, networkRepository, notificationGateway, pushGateway) {
        this.eventPublisher = eventPublisher;
        this.addressRepository = addressRepository;
        this.walletRepository = walletRepository;
        this.deviceRepository = deviceRepository;
        this.tokenRepository = tokenRepository;
        this.networkRepository = networkRepository;
        this.notificationGateway = notificationGateway;
        this.pushGateway = pushGateway;
    }
    start() {
        this.eventPublisher.subscribe('TransactionCreated', (event) => {
            void this.notify(event.transaction, 'transaction.created');
        });
        this.eventPublisher.subscribe('TransactionUpdated', (event) => {
            void this.notify(event.transaction, 'transaction.updated');
        });
    }
    async notify(transaction, type) {
        if (transaction.status !== 'confirmed') {
            return;
        }
        const addressIds = this.collectAddressIds(transaction);
        if (addressIds.length === 0) {
            return;
        }
        const activeDeviceIds = await this.resolveActiveDeviceIds(addressIds);
        const pushEnabledDeviceIds = this.pushGateway
            ? await this.resolvePushEnabledDeviceIds(addressIds)
            : [];
        if (activeDeviceIds.length === 0 && pushEnabledDeviceIds.length === 0) {
            return;
        }
        const baseMessage = {
            type,
            transaction: {
                id: transaction.id,
                hash: transaction.hash,
                status: transaction.status,
                block: transaction.block,
                networkId: transaction.networkId,
                timestamp: transaction.timestamp,
                from: transaction.from,
                to: transaction.to,
                interactedWith: transaction.interactedWith,
                value: transaction.value,
                fee: transaction.fee,
                method: transaction.method,
            },
        };
        for (const deviceId of activeDeviceIds) {
            this.notificationGateway.sendToDevice(deviceId, baseMessage);
        }
        if (pushEnabledDeviceIds.length === 0) {
            return;
        }
        const amount = await this.resolveAmount(transaction);
        const addressMap = await this.buildAddressMap(addressIds);
        const recipientMap = await this.buildRecipientMap(addressMap, transaction);
        const fromAddress = transaction.from
            ? addressMap.get(transaction.from)?.address ?? this.extractAddressFromData(transaction.data, 'from')
            : null;
        const toAddress = transaction.to
            ? addressMap.get(transaction.to)?.address ?? this.extractAddressFromData(transaction.data, 'to')
            : null;
        for (const deviceId of pushEnabledDeviceIds) {
            const recipient = recipientMap.get(deviceId);
            if (!recipient) {
                continue;
            }
            const pushMessage = {
                ...baseMessage,
                fromAddress,
                toAddress,
                recipient,
                amount: {
                    raw: transaction.value,
                    formatted: amount.formatted,
                    symbol: amount.symbol,
                },
            };
            this.pushGateway?.sendToDevice(deviceId, pushMessage);
        }
    }
    collectAddressIds(transaction) {
        const ids = new Set();
        if (transaction.from) {
            ids.add(transaction.from);
        }
        if (transaction.to) {
            ids.add(transaction.to);
        }
        for (const path of transaction.data) {
            if ((0, transaction_data_1.isContractTokenTransferPath)(path) && path.to) {
                ids.add(path.to);
            }
            if ((0, transaction_data_1.isNativeTransferPath)(path)) {
                if (path.from) {
                    ids.add(path.from);
                }
                if (path.to) {
                    ids.add(path.to);
                }
            }
        }
        return Array.from(ids);
    }
    async buildAddressMap(addressIds) {
        const map = new Map();
        for (const addressId of addressIds) {
            const address = await this.resolveAddress(addressId);
            if (address) {
                map.set(addressId, address);
            }
        }
        return map;
    }
    async resolveAddress(value) {
        const byId = await this.addressRepository.findById(value);
        if (byId) {
            return byId;
        }
        return this.addressRepository.findByAddress(value);
    }
    async buildRecipientMap(addressMap, transaction) {
        const recipients = new Map();
        const walletAddressIds = new Map();
        // Index both the internal id and the raw blockchain address so third-party
        // transactions that store raw addresses are matched correctly.
        for (const [key, address] of addressMap.entries()) {
            if (!walletAddressIds.has(address.walletId)) {
                walletAddressIds.set(address.walletId, new Set());
            }
            walletAddressIds.get(address.walletId)?.add(address.id);
            walletAddressIds.get(address.walletId)?.add(address.address);
            walletAddressIds.get(address.walletId)?.add(key);
        }
        const fromAddress = this.resolveCounterpartyAddress(transaction.from, addressMap);
        const toAddress = this.resolveCounterpartyAddress(transaction.to, addressMap);
        for (const [walletId, ownedAddressIds] of walletAddressIds.entries()) {
            const wallet = await this.walletRepository.findById(walletId);
            if (!wallet) {
                continue;
            }
            for (const deviceId of wallet.deviceIds) {
                const context = {
                    deviceId,
                    isSender: false,
                    isReceiver: false,
                    counterpartyAddress: null,
                };
                if (transaction.from && ownedAddressIds.has(transaction.from)) {
                    context.isSender = true;
                    context.counterpartyAddress = toAddress;
                }
                if (transaction.to && ownedAddressIds.has(transaction.to)) {
                    context.isReceiver = true;
                    context.counterpartyAddress = fromAddress;
                }
                // For smart-contract token transfers the recipient is recorded in
                // data[].to rather than transaction.to.
                for (const path of transaction.data) {
                    if ((0, transaction_data_1.isContractTokenTransferPath)(path) && path.to && ownedAddressIds.has(path.to)) {
                        context.isReceiver = true;
                        context.counterpartyAddress = fromAddress;
                    }
                    if ((0, transaction_data_1.isNativeTransferPath)(path)) {
                        if (path.from && ownedAddressIds.has(path.from)) {
                            context.isSender = true;
                            context.counterpartyAddress =
                                this.resolveCounterpartyAddress(path.to, addressMap) ?? toAddress;
                        }
                        if (path.to && ownedAddressIds.has(path.to)) {
                            context.isReceiver = true;
                            context.counterpartyAddress =
                                this.resolveCounterpartyAddress(path.from, addressMap) ?? fromAddress;
                        }
                    }
                }
                recipients.set(deviceId, context);
            }
        }
        return recipients;
    }
    resolveCounterpartyAddress(value, addressMap) {
        if (!value) {
            return null;
        }
        const resolved = addressMap.get(value);
        if (resolved) {
            return resolved.address;
        }
        // If the value is a raw blockchain address, use it directly so the
        // notification body can show a shortened address instead of "external".
        if (value.toLowerCase().startsWith('0x')) {
            return value;
        }
        return null;
    }
    extractAddressFromData(data, role) {
        for (const path of data) {
            if (role === 'from' && 'from' in path) {
                return path.from;
            }
            if (role === 'to' && 'to' in path) {
                return path.to;
            }
        }
        return null;
    }
    async resolveAmount(transaction) {
        if (transaction.interactedWith) {
            const token = await this.tokenRepository.findById(transaction.interactedWith);
            if (token) {
                const value = this.extractTokenTransferValue(transaction);
                return {
                    formatted: formatTokenValue(value, token.decimals),
                    symbol: token.symbol,
                };
            }
        }
        const network = await this.networkRepository.findById(transaction.networkId);
        const nativeToken = network
            ? (await this.tokenRepository.findByNetworkId(network.id)).find((t) => t.isNative)
            : null;
        if (nativeToken) {
            return {
                formatted: formatTokenValue(transaction.value, nativeToken.decimals),
                symbol: nativeToken.symbol,
            };
        }
        return {
            formatted: transaction.value,
            symbol: network?.code ?? 'units',
        };
    }
    extractTokenTransferValue(transaction) {
        for (const path of transaction.data) {
            if ((0, transaction_data_1.isContractTokenTransferPath)(path)) {
                return path.amount;
            }
        }
        return transaction.value;
    }
    async resolveActiveDeviceIds(addressIds) {
        return this.resolveDevicesByStatus(addressIds, ['active']);
    }
    async resolvePushEnabledDeviceIds(addressIds) {
        return this.resolveDevicesByStatus(addressIds, ['active', 'background']);
    }
    async resolveDevicesByStatus(addressIds, statuses) {
        const deviceIds = new Set();
        for (const addressId of addressIds) {
            const address = await this.resolveAddress(addressId);
            if (!address) {
                continue;
            }
            const wallet = await this.walletRepository.findById(address.walletId);
            if (!wallet) {
                continue;
            }
            for (const deviceId of wallet.deviceIds) {
                deviceIds.add(deviceId);
            }
        }
        const matchedDeviceIds = [];
        for (const deviceId of deviceIds) {
            const device = await this.deviceRepository.findById(deviceId);
            if (device && statuses.includes(device.status)) {
                matchedDeviceIds.push(deviceId);
            }
        }
        return matchedDeviceIds;
    }
}
exports.TransactionNotificationService = TransactionNotificationService;
//# sourceMappingURL=transaction-notification.service.js.map