/* eslint-disable max-depth */
import { TimeoutError, DialError, AbortError } from '@libp2p/interface';
import { PeerMap } from '@libp2p/peer-collections';
import { PriorityQueue } from '@libp2p/utils/priority-queue';
import { multiaddr } from '@multiformats/multiaddr';
import { Circuit } from '@multiformats/multiaddr-matcher';
import { anySignal } from 'any-signal';
import { setMaxListeners } from 'main-event';
import { CustomProgressEvent } from 'progress-events';
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string';
import { DialDeniedError, NoValidAddressesError } from '../errors.js';
import { getPeerAddress } from '../get-peer.js';
import { defaultAddressSorter } from './address-sorter.js';
import { DIAL_TIMEOUT, MAX_PARALLEL_DIALS, MAX_PEER_ADDRS_TO_DIAL, LAST_DIAL_FAILURE_KEY, MAX_DIAL_QUEUE_LENGTH, LAST_DIAL_SUCCESS_KEY } from './constants.js';
import { resolveMultiaddr, dnsaddrResolver } from './resolvers/index.js';
import { DEFAULT_DIAL_PRIORITY } from './index.js';
const defaultOptions = {
    maxParallelDials: MAX_PARALLEL_DIALS,
    maxDialQueueLength: MAX_DIAL_QUEUE_LENGTH,
    maxPeerAddrsToDial: MAX_PEER_ADDRS_TO_DIAL,
    dialTimeout: DIAL_TIMEOUT,
    resolvers: {
        dnsaddr: dnsaddrResolver
    }
};
export class DialQueue {
    queue;
    components;
    addressSorter;
    maxPeerAddrsToDial;
    maxDialQueueLength;
    dialTimeout;
    shutDownController;
    connections;
    log;
    resolvers;
    constructor(components, init = {}) {
        this.addressSorter = init.addressSorter;
        this.maxPeerAddrsToDial = init.maxPeerAddrsToDial ?? defaultOptions.maxPeerAddrsToDial;
        this.maxDialQueueLength = init.maxDialQueueLength ?? defaultOptions.maxDialQueueLength;
        this.dialTimeout = init.dialTimeout ?? defaultOptions.dialTimeout;
        this.connections = init.connections ?? new PeerMap();
        this.log = components.logger.forComponent('libp2p:connection-manager:dial-queue');
        this.components = components;
        this.resolvers = init.resolvers ?? defaultOptions.resolvers;
        this.shutDownController = new AbortController();
        setMaxListeners(Infinity, this.shutDownController.signal);
        // controls dial concurrency
        this.queue = new PriorityQueue({
            concurrency: init.maxParallelDials ?? defaultOptions.maxParallelDials,
            metricName: 'libp2p_dial_queue',
            metrics: components.metrics
        });
        // a started job errored
        this.queue.addEventListener('failure', (event) => {
            if (event.detail?.error.name !== AbortError.name) {
                this.log.error('error in dial queue - %e', event.detail);
            }
        });
    }
    start() {
        this.shutDownController = new AbortController();
        setMaxListeners(Infinity, this.shutDownController.signal);
    }
    /**
     * Clears any pending dials
     */
    stop() {
        this.shutDownController.abort();
        this.queue.abort();
    }
    /**
     * Connects to a given peer, multiaddr or list of multiaddrs.
     *
     * If a peer is passed, all known multiaddrs will be tried. If a multiaddr or
     * multiaddrs are passed only those will be dialled.
     *
     * Where a list of multiaddrs is passed, if any contain a peer id then all
     * multiaddrs in the list must contain the same peer id.
     *
     * The dial to the first address that is successfully able to upgrade a
     * connection will be used, all other dials will be aborted when that happens.
     */
    async dial(peerIdOrMultiaddr, options = {}) {
        const { peerId, multiaddrs } = getPeerAddress(peerIdOrMultiaddr);
        // make sure we don't have an existing non-limited connection to any of the
        // addresses we are about to dial
        const existingConnection = Array.from(this.connections.values()).flat().find(conn => {
            if (options.force === true) {
                return false;
            }
            if (conn.limits != null) {
                return false;
            }
            if (conn.remotePeer.equals(peerId)) {
                return true;
            }
            return multiaddrs.find(addr => {
                return addr.equals(conn.remoteAddr);
            });
        });
        if (existingConnection?.status === 'open') {
            this.log('already connected to %a', existingConnection.remoteAddr);
            options.onProgress?.(new CustomProgressEvent('dial-queue:already-connected'));
            return existingConnection;
        }
        // ready to dial, all async work finished - make sure we don't have any
        // pending dials in progress for this peer or set of multiaddrs
        const existingDial = this.queue.queue.find(job => {
            if (peerId?.equals(job.options.peerId) === true) {
                return true;
            }
            // does the dial contain any of the target multiaddrs?
            const addresses = job.options.multiaddrs;
            if (addresses == null) {
                return false;
            }
            for (const multiaddr of multiaddrs) {
                if (addresses.has(multiaddr.toString())) {
                    return true;
                }
            }
            return false;
        });
        if (existingDial != null) {
            this.log('joining existing dial target for %p', peerId);
            // add all multiaddrs to the dial target
            for (const multiaddr of multiaddrs) {
                existingDial.options.multiaddrs.add(multiaddr.toString());
            }
            options.onProgress?.(new CustomProgressEvent('dial-queue:already-in-dial-queue'));
            return existingDial.join(options);
        }
        if (this.queue.size >= this.maxDialQueueLength) {
            throw new DialError('Dial queue is full');
        }
        this.log('creating dial target for %p', peerId, multiaddrs.map(ma => ma.toString()));
        options.onProgress?.(new CustomProgressEvent('dial-queue:add-to-dial-queue'));
        return this.queue.add(async (options) => {
            options.onProgress?.(new CustomProgressEvent('dial-queue:start-dial'));
            // create abort conditions - need to do this before `calculateMultiaddrs` as
            // we may be about to resolve a dns addr which can time out
            const signal = anySignal([
                this.shutDownController.signal,
                options.signal
            ]);
            setMaxListeners(Infinity, signal);
            try {
                return await this.dialPeer(options, signal);
            }
            finally {
                // clean up abort signals/controllers
                signal.clear();
            }
        }, {
            peerId,
            priority: options.priority ?? DEFAULT_DIAL_PRIORITY,
            multiaddrs: new Set(multiaddrs.map(ma => ma.toString())),
            signal: options.signal ?? AbortSignal.timeout(this.dialTimeout),
            onProgress: options.onProgress
        });
    }
    async dialPeer(options, signal) {
        const peerId = options.peerId;
        const multiaddrs = options.multiaddrs;
        const failedMultiaddrs = new Set();
        // if we have no multiaddrs, only a peer id, set a flag so we will look the
        // peer up in the peer routing to obtain multiaddrs
        let forcePeerLookup = options.multiaddrs.size === 0;
        let dialed = 0;
        let dialIteration = 0;
        const errors = [];
        this.log('starting dial to %p', peerId);
        // repeat this operation in case addresses are added to the dial while we
        // resolve multiaddrs, etc
        while (forcePeerLookup || multiaddrs.size > 0) {
            dialIteration++;
            // only perform peer lookup once
            forcePeerLookup = false;
            // the addresses we will dial
            const addrsToDial = [];
            // copy the addresses into a new set
            const addrs = new Set(options.multiaddrs);
            // empty the old set - subsequent dial attempts for the same peer id may
            // add more addresses to try
            multiaddrs.clear();
            this.log('calculating addrs to dial %p from %s', peerId, [...addrs]);
            // load addresses from address book, resolve and dnsaddrs, filter
            // undialables, add peer IDs, etc
            const calculatedAddrs = await this.calculateMultiaddrs(peerId, addrs, {
                ...options,
                signal
            });
            for (const addr of calculatedAddrs) {
                // skip any addresses we have previously failed to dial
                if (failedMultiaddrs.has(addr.multiaddr.toString())) {
                    this.log.trace('skipping previously failed multiaddr %a while dialing %p', addr.multiaddr, peerId);
                    continue;
                }
                addrsToDial.push(addr);
            }
            this.log('%s dial to %p with %s', dialIteration === 1 ? 'starting' : 'continuing', peerId, addrsToDial.map(ma => ma.multiaddr.toString()));
            options?.onProgress?.(new CustomProgressEvent('dial-queue:calculated-addresses', addrsToDial));
            for (const address of addrsToDial) {
                if (dialed === this.maxPeerAddrsToDial) {
                    this.log('dialed maxPeerAddrsToDial (%d) addresses for %p, not trying any others', dialed, options.peerId);
                    throw new DialError('Peer had more than maxPeerAddrsToDial');
                }
                dialed++;
                try {
                    // try to dial the address
                    const conn = await this.components.transportManager.dial(address.multiaddr, {
                        ...options,
                        signal
                    });
                    this.log('dial to %a succeeded', address.multiaddr);
                    // record the successful dial and the address
                    try {
                        await this.components.peerStore.merge(conn.remotePeer, {
                            multiaddrs: [
                                conn.remoteAddr
                            ],
                            metadata: {
                                [LAST_DIAL_SUCCESS_KEY]: uint8ArrayFromString(Date.now().toString())
                            }
                        });
                    }
                    catch (err) {
                        this.log.error('could not update last dial failure key for %p', peerId, err);
                    }
                    // dial successful, return the connection
                    return conn;
                }
                catch (err) {
                    this.log.error('dial failed to %a', address.multiaddr, err);
                    // ensure we don't dial it again in this attempt
                    failedMultiaddrs.add(address.multiaddr.toString());
                    if (peerId != null) {
                        // record the failed dial
                        try {
                            await this.components.peerStore.merge(peerId, {
                                metadata: {
                                    [LAST_DIAL_FAILURE_KEY]: uint8ArrayFromString(Date.now().toString())
                                }
                            });
                        }
                        catch (err) {
                            this.log.error('could not update last dial failure key for %p', peerId, err);
                        }
                    }
                    // the user/dial timeout/shutdown controller signal aborted
                    if (signal.aborted) {
                        throw new TimeoutError(err.message);
                    }
                    errors.push(err);
                }
            }
        }
        if (errors.length === 1) {
            throw errors[0];
        }
        throw new AggregateError(errors, 'All multiaddr dials failed');
    }
    // eslint-disable-next-line complexity
    async calculateMultiaddrs(peerId, multiaddrs = new Set(), options = {}) {
        const addrs = [...multiaddrs].map(ma => ({
            multiaddr: multiaddr(ma),
            isCertified: false
        }));
        // if a peer id or multiaddr(s) with a peer id, make sure it isn't our peer id and that we are allowed to dial it
        if (peerId != null) {
            if (this.components.peerId.equals(peerId)) {
                throw new DialError('Tried to dial self');
            }
            if ((await this.components.connectionGater.denyDialPeer?.(peerId)) === true) {
                throw new DialDeniedError('The dial request is blocked by gater.allowDialPeer');
            }
            // if just a peer id was passed, load available multiaddrs for this peer
            // from the peer store
            if (addrs.length === 0) {
                this.log('loading multiaddrs for %p', peerId);
                try {
                    const peer = await this.components.peerStore.get(peerId);
                    addrs.push(...peer.addresses);
                    this.log('loaded multiaddrs for %p', peerId, addrs.map(({ multiaddr }) => multiaddr.toString()));
                }
                catch (err) {
                    if (err.name !== 'NotFoundError') {
                        throw err;
                    }
                }
            }
            // if we still don't have any addresses for this peer, or the only
            // addresses we have are without any routing information (e.g.
            // `/p2p/Qmfoo`), try a lookup using the peer routing
            if (addrs.length === 0) {
                this.log('looking up multiaddrs for %p in the peer routing', peerId);
                try {
                    const peerInfo = await this.components.peerRouting.findPeer(peerId, options);
                    this.log('found multiaddrs for %p in the peer routing', peerId, addrs.map(({ multiaddr }) => multiaddr.toString()));
                    addrs.push(...peerInfo.multiaddrs.map(multiaddr => ({
                        multiaddr,
                        isCertified: false
                    })));
                }
                catch (err) {
                    if (err.name === 'NoPeerRoutersError') {
                        this.log('no peer routers configured', peerId);
                    }
                    else {
                        this.log.error('looking up multiaddrs for %p in the peer routing failed - %e', peerId, err);
                    }
                }
            }
        }
        // resolve addresses - this can result in a one-to-many translation when
        // dnsaddrs are resolved
        let resolvedAddresses = (await Promise.all(addrs.map(async (addr) => {
            const result = await resolveMultiaddr(addr.multiaddr, this.resolvers, {
                dns: this.components.dns,
                log: this.log,
                ...options
            });
            if (result.length === 1 && result[0].equals(addr.multiaddr)) {
                return addr;
            }
            return result.map(multiaddr => ({
                multiaddr,
                isCertified: false
            }));
        })))
            .flat();
        // ensure the peer id is appended to the multiaddr
        if (peerId != null) {
            const peerIdMultiaddr = `/p2p/${peerId.toString()}`;
            resolvedAddresses = resolvedAddresses.map(addr => {
                const lastComponent = addr.multiaddr.getComponents().pop();
                // append peer id to multiaddr if it is not already present
                if (lastComponent?.name !== 'p2p') {
                    return {
                        multiaddr: addr.multiaddr.encapsulate(peerIdMultiaddr),
                        isCertified: addr.isCertified
                    };
                }
                return addr;
            });
        }
        const filteredAddrs = resolvedAddresses.filter(addr => {
            // filter out any multiaddrs that we do not have transports for
            if (this.components.transportManager.dialTransportForMultiaddr(addr.multiaddr) == null) {
                return false;
            }
            // if the resolved multiaddr has a PeerID but it's the wrong one, ignore it
            // - this can happen with addresses like bootstrap.libp2p.io that resolve
            // to multiple different peers
            const addrPeerId = addr.multiaddr.getPeerId();
            if (peerId != null && addrPeerId != null) {
                return peerId.equals(addrPeerId);
            }
            return true;
        });
        // deduplicate addresses
        const dedupedAddrs = new Map();
        for (const addr of filteredAddrs) {
            const maStr = addr.multiaddr.toString();
            const existing = dedupedAddrs.get(maStr);
            if (existing != null) {
                existing.isCertified = existing.isCertified || addr.isCertified || false;
                continue;
            }
            dedupedAddrs.set(maStr, addr);
        }
        const dedupedMultiaddrs = [...dedupedAddrs.values()];
        // make sure we actually have some addresses to dial
        if (dedupedMultiaddrs.length === 0) {
            throw new NoValidAddressesError('The dial request has no valid addresses');
        }
        const gatedAddrs = [];
        for (const addr of dedupedMultiaddrs) {
            if (this.components.connectionGater.denyDialMultiaddr != null && await this.components.connectionGater.denyDialMultiaddr(addr.multiaddr)) {
                continue;
            }
            gatedAddrs.push(addr);
        }
        const sortedGatedAddrs = this.addressSorter == null ? defaultAddressSorter(gatedAddrs) : gatedAddrs.sort(this.addressSorter);
        // make sure we actually have some addresses to dial
        if (sortedGatedAddrs.length === 0) {
            throw new DialDeniedError('The connection gater denied all addresses in the dial request');
        }
        this.log.trace('addresses for %p before filtering', peerId ?? 'unknown peer', resolvedAddresses.map(({ multiaddr }) => multiaddr.toString()));
        this.log.trace('addresses for %p after filtering', peerId ?? 'unknown peer', sortedGatedAddrs.map(({ multiaddr }) => multiaddr.toString()));
        return sortedGatedAddrs;
    }
    async isDialable(multiaddr, options = {}) {
        if (!Array.isArray(multiaddr)) {
            multiaddr = [multiaddr];
        }
        try {
            const addresses = await this.calculateMultiaddrs(undefined, new Set(multiaddr.map(ma => ma.toString())), options);
            if (options.runOnLimitedConnection === false) {
                // return true if any resolved multiaddrs are not relay addresses
                return addresses.find(addr => {
                    return !Circuit.matches(addr.multiaddr);
                }) != null;
            }
            return true;
        }
        catch (err) {
            this.log.trace('error calculating if multiaddr(s) were dialable', err);
        }
        return false;
    }
}
//# sourceMappingURL=dial-queue.js.map