import { PeerHostRecord } from "../PeerHostRecord";
import { promises as dnsPromises, SrvRecord } from "dns";

import bech32 from "bech32";
import { AddressType } from "../domain/AddressType";
export interface DnsPeerQueryOptions {
    /**
     * The domain of the dns seed.
     */
    dnsSeed: string;

    /**
     * The realm byte used to specify what realm the returned nodes must support.
     */
    realm?: number;

    /**
     * The address type bit field specifies the address types that should be returned.
     * It follows the format of the address descriptor type specified in BOLT #7.
     */
    addressTypes?: AddressType[];

    /**
     * The bech32-encoded node id used to retrive the result of a single node.
     */
    nodeId?: string;

    /**
     * The number of desired reply records.
     */
    desiredReplyRecords?: number;
}

/**
 * This class implements the node discovery mechanism described
 * in BOLT #10.
 * The purpose of this component is to assist in the initial
 * node discovery process for nodes that have no known contacts,
 * and to help nodes discover the current network address of previously
 * known peers. A domain name server that implements BOLT #10 is
 * referred to as a DNS Seed and answers incoming DNS queries of
 * type A, AAAA, or SRV.
 */
export class DnsPeerQuery {
    private resolver: dnsPromises.Resolver;

    constructor(resolver?: dnsPromises.Resolver) {
        this.resolver = resolver || new dnsPromises.Resolver();
    }

    /**
     * The query method starts by querying a DNS seed for SRV records.
     * This will result in a list of returned SRV records that each
     * represent a lightning node that can be connected to. Each record
     * is a subdomain of the dns seed where the first component of the
     * subdomain is the bech32 encoded public key of the node (also known
     * as the node_id). Each subdomain can subsequently be used to query
     * the DNS seed for the A (or AAA) records of the lightning node with
     * the specified node_id. The query method ends by constructing peer
     * host records using the port from the SRV records and the ip
     * address from the A (or AAA) records.
     * @param dnsPeerQueryOptions
     * @returns valid peer host records
     */
    public async query(dnsPeerQueryOptions: DnsPeerQueryOptions): Promise<PeerHostRecord[]> {
        const dnsSeed = this._buildUrl(dnsPeerQueryOptions);
        const peerSrvRecords: SrvRecord[] = await this._getPeerSrvRecords(dnsSeed);
        const peerHostRecordPromises: Promise<PeerHostRecord>[] = [];

        for (const peerSrvRecord of peerSrvRecords) {
            peerHostRecordPromises.push(this._createPeerHostRecord(peerSrvRecord));
        }

        const peerHostRecordSettlements = await Promise.allSettled(peerHostRecordPromises);

        const peers = peerHostRecordSettlements
            .filter(recordSettlement => recordSettlement?.status == "fulfilled")
            .map((p: PromiseFulfilledResult<PeerHostRecord>) => p.value);

        return peers;
    }

    private async _createPeerHostRecord(peerSrvRecord: SrvRecord): Promise<PeerHostRecord> {
        const peerAddress = await this._resolveSrvNameToIp(peerSrvRecord.name);

        return new PeerHostRecord(
            this._getPublicKeyFromSrvRecord(peerSrvRecord),
            peerAddress,
            peerSrvRecord.port,
        );
    }

    private _buildUrl(dnsPeerQueryOptions: DnsPeerQueryOptions): string {
        let { dnsSeed } = dnsPeerQueryOptions;
        const { realm, addressTypes, nodeId, desiredReplyRecords } = dnsPeerQueryOptions;

        if (desiredReplyRecords != null) {
            dnsSeed = `n${desiredReplyRecords}.${dnsSeed}`;
        }

        if (nodeId != null) {
            dnsSeed = `l${nodeId}.${dnsSeed}`;
        }

        if (addressTypes != null) {
            const a = addressTypes.reduce((bitField, addressType) => {
                return bitField | (1 << addressType);
            }, 0);

            dnsSeed = `a${a}.${dnsSeed}`;
        }

        if (realm != null) {
            dnsSeed = `r${realm}.${dnsSeed}`;
        }

        return dnsSeed;
    }

    private _getPublicKeyFromSrvRecord(srvRecord: SrvRecord): Buffer {
        const domainComponents = srvRecord.name.split(".");
        const { words } = bech32.decode(domainComponents[0]);
        return Buffer.from(bech32.fromWords(words));
    }

    private _getPeerSrvRecords(dnsSeed: string): Promise<SrvRecord[]> {
        return this.resolver.resolveSrv(dnsSeed);
    }

    private async _resolveSrvNameToIp(hostname: string): Promise<string> {
        const ipAddresses: string[] = await this.resolver.resolve(hostname, "A");
        return ipAddresses?.[0];
    }
}
