import assert from 'node:assert';
import BoxError from '../boxerror.js';
import logger from '../logger.js';
import dig from '../dig.js';
import dns from 'node:dns';
import promiseRetry from '../promise-retry.js';
import safe from 'safetydance';
import _ from '../underscore.js';

const { log } = logger('dns/waitfordns');

async function resolveIp(hostname, type, options) {
    assert.strictEqual(typeof hostname, 'string');
    assert(type === 'A' || type === 'AAAA');
    assert.strictEqual(typeof options, 'object');

    // try A record at authoritative server
    log(`resolveIp: Checking ${type} for ${hostname} at ${options.server}`);
    const [error, results] = await safe(dig.resolve(hostname, type, options));
    if (!error && results.length !== 0) return results;

    // try CNAME record at authoritative server
    log(`resolveIp: No ${type}. Checking CNAME for ${hostname} at ${options.server}`);
    const cnameResults = await dig.resolve(hostname, 'CNAME', options);
    if (cnameResults.length === 0) return cnameResults;

    // recurse lookup the CNAME record
    log(`resolveIp: found CNAME for ${hostname}. resolving ${cnameResults[0]}`);
    return await dig.resolve(cnameResults[0], type, _.omit(options, ['server']));
}

async function isChangeSynced(hostname, type, value, nameserver) {
    assert.strictEqual(typeof hostname, 'string');
    assert.strictEqual(typeof type, 'string');
    assert.strictEqual(typeof value, 'string');
    assert.strictEqual(typeof nameserver, 'string');

    // ns records cannot have cname
    const [error4, nsIPv4s] = await safe(dig.resolve(nameserver, 'A', { timeout: 5000 }));
    const [error6, nsIPv6s] = await safe(dig.resolve(nameserver, 'AAAA', { timeout: 5000 }));

    if (error4 && error6) {
        log(`isChangeSynced: cannot resolve NS ${nameserver}`); // NS doesn't resolve at all; it's fine
        return true;
    }

    const nsIPs = [].concat(nsIPv4s || []).concat(nsIPv6s || []);
    const status = [];
    for (let i = 0; i < nsIPs.length; i++) {
        const nsIp = nsIPs[i];
        const resolveOptions = { server: nsIp, timeout: 5000, tries: 1 };
        const resolver = type === 'A' || type === 'AAAA' ? resolveIp(hostname, type, resolveOptions) : dig.resolve(hostname, 'TXT', resolveOptions);

        const [error, answer] = await safe(resolver);
        // CONNREFUSED - when there is no ipv4/ipv6 connectivity. REFUSED - server won't answer maybe by policy
        if (error && (error.code === dns.TIMEOUT || error.code === dns.REFUSED || error.code === dns.CONNREFUSED)) {
            log(`isChangeSynced: NS ${nameserver} (${nsIp}) not resolving ${hostname} (${type}): ${error}. Ignoring`);
            status[i] = true; // should be ok if dns server is down
            continue;
        }

        if (error) {
            log(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${hostname} (${type}): ${error}`);
            status[i] = false;
            continue;
        }

        let match;
        if (type === 'A' || type === 'AAAA') {
            match = answer.length === 1 && answer[0] === value;
        } else if (type === 'TXT') { // answer is a 2d array of strings
            match = answer.some(function (a) { return value === a.join(''); });
        }

        log(`isChangeSynced: ${hostname} (${type}) was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}. Match ${match}`);
        status[i] = match;
    }

    return status.every(s => s === true);
}

// check if IP change has propagated to every nameserver
async function waitForDns(hostname, zoneName, type, value, options) {
    assert.strictEqual(typeof hostname, 'string');
    assert.strictEqual(typeof zoneName, 'string');
    assert(type === 'A' || type === 'AAAA' || type === 'TXT');
    assert.strictEqual(typeof value, 'string');
    assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }

    log(`waitForDns: waiting for ${hostname} to be ${value} in zone ${zoneName}`);

    await promiseRetry(Object.assign({ log }, options), async function () {
        const nameservers = await dig.resolve(zoneName, 'NS', { timeout: 5000 });
        if (!nameservers) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to get nameservers');
        log(`waitForDns: nameservers are ${JSON.stringify(nameservers)}`);

        for (const nameserver of nameservers) {
            const synced = await isChangeSynced(hostname, type, value, nameserver);
            log(`waitForDns: ${hostname} at ns ${nameserver}: ${synced ? 'done' : 'not done'} `);
            if (!synced) throw new BoxError(BoxError.EXTERNAL_ERROR, 'ETRYAGAIN');
        }
    });

    log(`waitForDns: ${hostname} has propagated`);
}

export default waitForDns;
