/**
 * Transactions Resource
 *
 * Handles transaction history fetching and management.
 * Uses IndexerClient for HTTP queries and KeyVault for crypto operations.
 */
import { ApiError } from '../api/base-client';
import { TransactionType, TransactionStatus, TransactionDirection, } from '../types';
import { silentLogger } from '../utils/logger';
/**
 * Transactions Resource for transaction history operations
 */
const TAG = 'Transactions';
function isPendingLikeStatus(status) {
    return status === 'pending' || status === 'processing' || status === 'preconfirmed';
}
function mapRemoteStatusToLocal(status) {
    switch (status) {
        case 'completed':
            return TransactionStatus.completed;
        case 'failed':
            return TransactionStatus.failed;
        case 'preconfirmed':
            return TransactionStatus.preconfirmed;
        case 'pending':
        case 'processing':
        default:
            return TransactionStatus.pending;
    }
}
export class TransactionsResource {
    indexer;
    keyVault;
    getMpk;
    stores;
    log;
    previousPendingHashes = new Set();
    _onTransactionResolved;
    constructor(indexer, keyVault, stores, getMpk, logger) {
        this.indexer = indexer;
        this.keyVault = keyVault;
        this.getMpk = getMpk;
        this.stores = stores;
        this.log = logger ?? silentLogger;
    }
    /**
     * Register a callback invoked when a pending transaction resolves
     * (status changes from pending to completed/failed).
     *
     * Used internally to trigger balance syncs when transactions confirm.
     */
    setOnTransactionResolved(handler) {
        this._onTransactionResolved = handler;
    }
    get transactionStore() {
        return this.stores.transaction;
    }
    /**
     * Build a tokenId → token info map from registered tokens. The underlying
     * `getRegisteredTokens` call is cached + deduped at the IndexerClient
     * layer, so this stays cheap to call repeatedly.
     */
    async resolveTokenMap() {
        const registered = await this.indexer.getRegisteredTokens();
        const map = new Map();
        for (const token of registered) {
            map.set(token.id, {
                address: token.tokenAddress,
                symbol: token.symbol ?? '',
                decimals: token.decimals ?? 18,
            });
        }
        return map;
    }
    /**
     * Map raw API Transaction[] to PendingTransaction[].
     */
    mapToTransactions(data, tokenMap) {
        return data.map((tx) => {
            const tokenMeta = tokenMap.get(tx.tokenId);
            let type;
            if (tx.txType === 'shield')
                type = TransactionType.shield;
            else if (tx.txType === 'unshield')
                type = TransactionType.unshield;
            else
                type = TransactionType.transfer;
            const isIncoming = tx.direction === 'incoming';
            // The indexer attaches counterparty at the note level, not the top
            // level. For an outgoing tx the recipient lives on the non-change
            // "outgoing" note; for an incoming tx the sender lives on the
            // non-change "incoming" note.
            const wantNoteDirection = isIncoming ? 'incoming' : 'outgoing';
            const counterpartyNote = tx.notes?.find((n) => !n.isChange && n.direction === wantNoteDirection && n.counterparty);
            const counterparty = counterpartyNote?.counterparty;
            // Omit `from`/`to` keys when unknown so a subsequent
            // `mergeTransactions` spread does not clobber values populated locally
            // at submission time (e.g. `tx.to` set in `Vault.send`).
            const mapped = {
                txHash: tx.txHash,
                type,
                status: TransactionStatus.completed,
                direction: isIncoming
                    ? TransactionDirection.incoming
                    : TransactionDirection.outgoing,
                tokenAddress: (tokenMeta?.address ?? ''),
                tokenSymbol: tokenMeta?.symbol,
                amount: tx.value ? BigInt(tx.value) : undefined,
                receivers: [],
                createdAt: tx.createdAt * 1000,
                updatedAt: tx.createdAt * 1000,
            };
            if (counterparty !== undefined) {
                if (isIncoming)
                    mapped.from = counterparty;
                else
                    mapped.to = counterparty;
            }
            return mapped;
        });
    }
    /**
     * Fetch grouped wallet transactions from the indexer.
     *
     * Returns transactions grouped by txHash with UTXO-level note details.
     * Also maps the raw data to `PendingTransaction[]` and merges them into
     * the transaction store for reactive UI updates.
     *
     * Requires authentication (returns empty if no MPK is available).
     *
     * @param params - Optional filters (txType, tokenId, direction, limit, cursor for pagination)
     * @returns Paginated transaction response
     *
     * @example
     * ```typescript
     * const history = await sdk.transactions.fetchHistory({ limit: 20, direction: 'incoming' });
     * for (const tx of history.data) {
     *   console.log(tx.txHash, tx.type, tx.status);
     * }
     * // They're also merged into the transaction store automatically.
     * ```
     */
    async fetchHistory(params) {
        const mpk = this.getMpk();
        if (!mpk) {
            return { data: [], limit: 0, transactions: [] };
        }
        const response = await this.indexer.getWalletTransactions(mpk, params);
        // Map raw data to PendingTransaction[] and merge into store
        let mapped = [];
        try {
            const tokenMap = await this.resolveTokenMap();
            mapped = this.mapToTransactions(response.data, tokenMap);
            if (mapped.length > 0) {
                this.transactionStore.getState().mergeTransactions(mapped);
            }
        }
        catch (e) {
            this.log.warn(TAG, 'Failed to map/store history transactions', e);
        }
        return { ...response, transactions: mapped };
    }
    getByHash(txHash) {
        return this.transactionStore.getState().getTransaction(txHash);
    }
    /**
     * Poll status endpoints for all pending transactions.
     * Updates the Zustand store to reflect resolved transactions.
     *
     * Returns the number of resolved transactions.
     */
    async refreshPendingTransactions() {
        try {
            const pending = this.transactionStore.getState().pending;
            let resolved = 0;
            for (const tx of pending) {
                if (tx.status !== TransactionStatus.pending || !tx.requestId)
                    continue;
                try {
                    const updated = await this.pollTransactionStatus(tx);
                    if (updated) {
                        resolved++;
                        // Notify listener so balances can be synced for the affected token
                        if (this._onTransactionResolved) {
                            try {
                                // Re-read the transaction from the store to get the updated status
                                const resolvedTx = this.transactionStore.getState().getTransaction(tx.txHash) ?? tx;
                                this._onTransactionResolved(resolvedTx);
                            }
                            catch (e) {
                                this.log.warn(TAG, 'onTransactionResolved callback failed', e);
                            }
                        }
                    }
                }
                catch (error) {
                    this.log.warn(TAG, `Failed to poll status for ${tx.txHash}`, error);
                }
            }
            if (resolved > 0) {
                this.log.debug(TAG, `${resolved} transaction(s) resolved`);
            }
            return resolved;
        }
        catch (error) {
            this.log.warn(TAG, 'Failed to refresh pending transactions', error);
            return 0;
        }
    }
    /**
     * Poll status for a single pending transaction and update the store.
     * Returns true if the transaction status changed from pending.
     */
    async pollTransactionStatus(tx) {
        const requestId = tx.requestId;
        switch (tx.type) {
            case TransactionType.shield: {
                try {
                    const status = await this.indexer.getShieldStatus(requestId);
                    if (!isPendingLikeStatus(status.status)) {
                        this.transactionStore.getState().updateTransaction(tx.txHash, {
                            status: mapRemoteStatusToLocal(status.status),
                            updatedAt: Date.now(),
                        }, status.txHash);
                        return true;
                    }
                    return false;
                }
                catch (error) {
                    if (error instanceof ApiError && error.status === 404) {
                        this.log.debug(TAG, `Shield ${requestId} not indexed yet; keeping pending`);
                        return false;
                    }
                    throw error;
                }
            }
            case TransactionType.transfer: {
                const status = await this.indexer.getTransferStatus(requestId);
                if (!isPendingLikeStatus(status.status)) {
                    this.transactionStore.getState().updateTransaction(tx.txHash, {
                        status: mapRemoteStatusToLocal(status.status),
                        updatedAt: Date.now(),
                    }, status.txHash);
                    return true;
                }
                return false;
            }
            case TransactionType.unshield: {
                const status = await this.indexer.getUnshieldStatus(requestId);
                if (!isPendingLikeStatus(status.status)) {
                    this.transactionStore.getState().updateTransaction(tx.txHash, {
                        status: mapRemoteStatusToLocal(status.status),
                        updatedAt: Date.now(),
                    }, status.txHash);
                    return true;
                }
                return false;
            }
            default:
                return false;
        }
    }
    /**
     * Poll pending transactions and return both the current pending list and
     * any transactions that resolved since the last poll.
     *
     * The first call always returns an empty `resolved` array (no previous state).
     *
     * @returns `{ pending, resolved }` — current pending txs and newly resolved ones
     *
     * @example
     * ```typescript
     * const { pending, resolved } = await sdk.transactions.poll();
     * for (const tx of resolved) {
     *   console.log(`${tx.txHash} is now ${tx.status}`);
     * }
     * ```
     */
    async poll() {
        // Snapshot previous pending before refresh so we can diff afterwards.
        const previousPending = new Map();
        for (const hash of this.previousPendingHashes) {
            const tx = this.transactionStore.getState().getTransaction(hash);
            if (tx)
                previousPending.set(hash, tx);
        }
        await this.refreshPendingTransactions();
        // Get current pending from store
        const currentPending = this.transactionStore.getState().pending;
        const currentHashes = new Set(currentPending.map(tx => tx.txHash));
        // Compute resolved: was pending last poll, no longer pending now
        const resolved = [];
        for (const [hash, tx] of previousPending) {
            if (!currentHashes.has(hash)) {
                resolved.push(tx);
            }
        }
        this.previousPendingHashes = currentHashes;
        return { pending: currentPending, resolved };
    }
    async getNotes(tokenAddress) {
        const mpk = this.getMpk();
        if (!mpk) {
            return [];
        }
        try {
            const [rawNotes, registeredTokens] = await Promise.all([
                this.indexer.getUnspentNotes(mpk, tokenAddress),
                this.indexer.getRegisteredTokens(),
            ]);
            const tokenById = new Map(registeredTokens.map((token) => [token.id, token]));
            return rawNotes.data
                .map((note) => {
                const resolvedAddress = tokenById.get(note.tokenId)?.tokenAddress ?? tokenAddress;
                if (!resolvedAddress) {
                    this.log.warn(TAG, `Skipping note with unresolved tokenId ${note.tokenId}`);
                    return null;
                }
                return {
                    commitment: note.commitment,
                    value: note.amount,
                    tokenAddress: resolvedAddress,
                    tokenId: String(note.tokenId),
                    random: note.noteRnd,
                    owner: note.accountId,
                    spent: note.isSpent,
                    isOpaque: note.isOpaque,
                    // Wallet note responses do not currently include a timestamp.
                    createdAt: 0,
                };
            })
                .filter((note) => note !== null);
        }
        catch (error) {
            this.log.error(TAG, 'Failed to fetch notes', error);
            return [];
        }
    }
    /**
     * Get the status of a shield by its on-chain request ID.
     */
    async getShieldStatus(requestId) {
        return this.indexer.getShieldStatus(requestId);
    }
    /**
     * Get the status of a transfer by its queue ID.
     */
    async getTransferStatus(id) {
        return this.indexer.getTransferStatus(id);
    }
    /**
     * Get the status of a unshield by its queue ID.
     */
    async getUnshieldStatus(id) {
        return this.indexer.getUnshieldStatus(id);
    }
}
//# sourceMappingURL=Transactions.js.map