/**
 * Function/Network/dns-lookup.js
 * ---------------------------------------------------------------------------
 * DNS LOOKUP
 *
 * Resolves a domain to IPv4 and IPv6 addresses and, on request, its mail,
 * name server and TXT records. Uses the Node.js resolver - no shell, no
 * external tool. Read-only.
 * ---------------------------------------------------------------------------
 */

'use strict';

const dns = require('dns').promises;
const kit = require('../../core/kit');

/** Reject anything that is not a plausible host name before resolving. */
const HOST_PATTERN = /^(?!-)[A-Za-z0-9-_.]{1,253}$/;

async function safeResolve(fn) {
  try {
    return { ok: true, value: await fn() };
  } catch (err) {
    return { ok: false, error: err.code || err.message };
  }
}

async function lookup(domain, options = {}) {
  const [v4, v6] = await Promise.all([
    safeResolve(() => dns.resolve4(domain, { ttl: true })),
    safeResolve(() => dns.resolve6(domain)),
  ]);

  const extra = {};
  if (options.records) {
    const [mx, ns, txt, cname] = await Promise.all([
      safeResolve(() => dns.resolveMx(domain)),
      safeResolve(() => dns.resolveNs(domain)),
      safeResolve(() => dns.resolveTxt(domain)),
      safeResolve(() => dns.resolveCname(domain)),
    ]);
    extra.mx = mx;
    extra.ns = ns;
    extra.txt = txt;
    extra.cname = cname;
  }

  return { domain, v4, v6, ...extra, servers: dns.getServers() };
}

module.exports = {
  name: 'DNS Lookup',
  menuName: 'DNS Lookup',
  description: 'Resolves a domain to IPv4/IPv6 addresses and can show MX, NS, TXT and CNAME records.',
  version: '1.0.0',
  author: 'InfinitySniper HFX',
  category: 'Network',
  order: 30,
  requiredPermissions: 'normal',
  destructive: false,
  dependencies: [],
  minimumAppVersion: '1.0.0',
  permissions: ['network:dns'],
  tags: ['dns', 'domain', 'lookup', 'resolve', 'network'],
  documentation:
    'Resolves a domain name with the DNS servers this machine is configured to use and shows the ' +
    'IPv4 addresses with their TTL, the IPv6 addresses and, if asked, the mail (MX), name server ' +
    '(NS), text (TXT) and alias (CNAME) records. Reverse lookups of an IP address are supported too.',

  async selfTest() {
    const result = await safeResolve(() => dns.resolve4('localhost'));
    // localhost may be resolved from the hosts file rather than DNS - either is fine.
    return { ok: true, message: result.ok ? 'resolver responding' : 'resolver reachable (no DNS answer for localhost)' };
  },

  async execute(ctx) {
    const { ui, prompt } = ctx;
    kit.header('DNS Lookup', ['Resolve a domain name to its addresses and records.']);

    for (;;) {
      const domain = (await prompt.text('Domain name or IP address (empty to go back)')).trim();
      if (!domain) break;

      // Reverse lookup when the input is an IP address.
      if (/^\d+\.\d+\.\d+\.\d+$/.test(domain)) {
        const spin = ui.spinner(`Reverse lookup for ${domain}...`);
        const reverse = await safeResolve(() => dns.reverse(domain));
        spin.stop();

        ui.blank();
        ui.section('REVERSE LOOKUP');
        ui.blank();
        kit.kv('IP Address', domain);
        kit.kv('DNS Resolution', reverse.ok ? 'Successful' : `Failed (${reverse.error})`, reverse.ok ? 'brightGreen' : 'brightRed');
        if (reverse.ok) {
          reverse.value.forEach((host, index) => kit.kv(`Host Name ${index + 1}`, host));
        }
        ui.blank();
        continue;
      }

      if (!HOST_PATTERN.test(domain)) {
        ui.blank();
        ui.error('That does not look like a valid domain name.');
        continue;
      }

      const withRecords = await prompt.confirm('Also look up MX, NS, TXT and CNAME records?');
      const spin = ui.spinner(`Resolving ${domain}...`);
      const result = await lookup(domain, { records: withRecords === true });
      spin.stop();

      ui.blank();
      ui.section('RESULT');
      ui.blank();
      kit.kv('Domain', domain);
      kit.kv(
        'DNS Resolution Status',
        result.v4.ok || result.v6.ok ? 'Successful' : `Failed (${result.v4.error || result.v6.error})`,
        result.v4.ok || result.v6.ok ? 'brightGreen' : 'brightRed'
      );
      kit.kv('DNS Servers Used', result.servers.slice(0, 3).join(', '));

      ui.blank();
      ui.section('IPv4 RESULTS');
      ui.blank();
      if (result.v4.ok && result.v4.value.length) {
        for (const entry of result.v4.value) {
          kit.kv(entry.address, `TTL ${entry.ttl}s`);
        }
      } else {
        ui.println('  ' + ui.c.gray(`No IPv4 record (${result.v4.error || 'none'})`));
      }

      ui.blank();
      ui.section('IPv6 RESULTS');
      ui.blank();
      if (result.v6.ok && result.v6.value.length) {
        for (const address of result.v6.value) ui.println('  ' + ui.c.brightWhite(address));
      } else {
        ui.println('  ' + ui.c.gray(`No IPv6 record (${result.v6.error || 'none'})`));
      }

      if (withRecords === true) {
        ui.blank();
        ui.section('OTHER RECORDS');
        ui.blank();
        if (result.mx.ok && result.mx.value.length) {
          for (const record of result.mx.value) kit.kv(`MX (priority ${record.priority})`, record.exchange);
        } else {
          kit.kv('MX', null);
        }
        if (result.ns.ok && result.ns.value.length) {
          result.ns.value.forEach((server, index) => kit.kv(`NS ${index + 1}`, server));
        } else {
          kit.kv('NS', null);
        }
        if (result.cname.ok && result.cname.value.length) {
          result.cname.value.forEach((name, index) => kit.kv(`CNAME ${index + 1}`, name));
        }
        if (result.txt.ok && result.txt.value.length) {
          result.txt.value.slice(0, 6).forEach((chunks, index) => {
            kit.kv(`TXT ${index + 1}`, ui.truncate(chunks.join(''), 44));
          });
        }
      }

      await kit.offerExport('DNS Lookup Report', 'DnsLookup', (report) => {
        const section = report.section('DNS LOOKUP');
        section.add('Domain', domain);
        section.add('Resolution Status', result.v4.ok || result.v6.ok ? 'Successful' : 'Failed');
        section.add('DNS Servers Used', result.servers.join(', '));

        const v4 = report.section('IPv4 RESULTS');
        if (result.v4.ok) result.v4.value.forEach((entry, index) => v4.add(`Address ${index + 1}`, `${entry.address} (TTL ${entry.ttl}s)`));
        else v4.note(`No IPv4 record (${result.v4.error})`);

        const v6 = report.section('IPv6 RESULTS');
        if (result.v6.ok) result.v6.value.forEach((address, index) => v6.add(`Address ${index + 1}`, address));
        else v6.note(`No IPv6 record (${result.v6.error})`);

        if (withRecords === true) {
          const other = report.section('OTHER RECORDS');
          if (result.mx.ok) result.mx.value.forEach((r) => other.add(`MX ${r.priority}`, r.exchange));
          if (result.ns.ok) result.ns.value.forEach((r, i) => other.add(`NS ${i + 1}`, r));
          if (result.txt.ok) result.txt.value.forEach((r, i) => other.add(`TXT ${i + 1}`, r.join('')));
        }
      });

      ui.blank();
      const again = await prompt.confirm('Look up another domain?');
      if (again !== true) break;
    }

    return kit.done('DNS lookup finished.');
  },
};
