ionValue } = require('internal/options');
const {
  validateArray,
  validateInt32,
  validateOneOf,
  validateString,
} = require('internal/validators');
let binding;
function lazyBinding() {
  binding ??= internalBinding('cares_wrap');
  return binding;
}
const IANA_DNS_PORT = 53;
const IPv6RE = /^\[([^[\]]*)\]/;
const addrSplitRE = /(^.+?)(?::(\d+))?$/;

const {
  namespace: {
    addSerializeCallback,
    addDeserializeCallback,
    isBuildingSnapshot,
  },
} = require('internal/v8/startup_snapshot');

function validateTimeout(options) {
  const { timeout = -1 } = { ...options };
  validateInt32(timeout, 'options.timeout', -1);
  return timeout;
}

function validateTries(options) {
  const { tries = 4 } = { ...options };
  validateInt32(tries, 'options.tries', 1);
  return tries;
}

const kSerializeResolver = Symbol('dns:resolver:serialize');
const kDeserializeResolver = Symbol('dns:resolver:deserialize');
const kSnapshotStates = Symbol('dns:resolver:config');
const kInitializeHandle = Symbol('dns:resolver:initializeHandle');
const kSetServersInternal = Symbol('dns:resolver:setServers');

// Resolver instances correspond 1:1 to c-ares channels.

class ResolverBase {
  constructor(options = undefined) {
    const timeout = validateTimeout(options);
    const tries = validateTries(options);
    // If we are building snapshot, save the states of the resolver along
    // the way.
    if (isBuildingSnapshot()) {
      this[kSnapshotStates] = { timeout, tries };
    }
    this[kInitializeHandle](timeout, tries);
  }

  [kInitializeHandle](timeout, tries) {
    const { ChannelWrap } = lazyBinding();
    this._handle = new ChannelWrap(timeout, tries);
  }

  cancel() {
    this._handle.cancel();
  }

  getServers() {
    return ArrayPrototypeMap(this._handle.getServers() || [], (val) => {
      if (!val[1] || val[1] === IANA_DNS_PORT)
        return val[0];

      const host = isIP(val[0]) === 6 ? `[${val[0]}]` : val[0];
      return `${host}:${val[1]}`;
    });
  }

  setServers(servers) {
    validateArray(servers, 'servers');

    // Cache the original servers because in the event of an error while
    // setting the servers, c-ares won't have any servers available for
    // resolution.
    const newSet = [];
    ArrayPrototypeForEach(servers, (serv, index) => {
      validateString(serv, `servers[${index}]`);
      let ipVersion = isIP(serv);

      if (ipVersion !== 0)
        return ArrayPrototypePush(newSet, [ipVersion, serv, IANA_DNS_PORT]);

      const match = RegExpPrototypeExec(IPv6RE, serv);

      // Check for an IPv6 in brackets.
      if (match) {
        ipVersion = isIP(match[1]);

        if (ipVersion !== 0) {
          const port = NumberParseInt(
            RegExpPrototypeSymbolReplace(addrSplitRE, serv, '$2')) || IANA_DNS_PORT;
          return ArrayPrototypePush(newSet, [ipVersion, match[1], port]);
        }
      }

      // addr::port
      const addrSplitMatch = RegExpPrototypeExec(addrSplitRE, serv);

      if (addrSplitMatch) {
        const hostIP = addrSplitMatch[1];
        const port = addrSplitMatch[2] || IANA_DNS_PORT;

        ipVersion = isIP(hostIP);

        if (ipVersion !== 0) {
          return ArrayPrototypePush(
            newSet, [ipVersion, hostIP, NumberParseInt(port)]);
        }
      }

      throw new ERR_INVALID_IP_ADDRESS(serv);
    });

    this[kSetServersInternal](newSet, servers);
  }

  [kSetServersInternal](newSet, servers) {
    const orig = ArrayPrototypeMap(this._handle.getServers() || [], (val) => {
      val.unshift(isIP(val[0]));
      return val;
    });
    const errorNumber = this._handle.setServers(newSet);

    if (errorNumber !== 0) {
      // Reset the servers to the old servers, because ares probably unset them.
      this._handle.setServers(orig);
      const { strerror } = lazyBinding();
      const err = strerror(errorNumber);
      throw new ERR_DNS_SET_SERVERS_FAILED(err, servers);
    }

    if (isBuildingSnapshot()) {
      this[kSnapshotStates].servers = newSet;
    }
  }


  setLocalAddress(ipv4, ipv6) {
    validateString(ipv4, 'ipv4');

    if (ipv6 !== undefined) {
      validateString(ipv6, 'ipv6');
    }

    this._handle.setLocalAddress(ipv4, ipv6);

    if (isBuildingSnapshot()) {
      this[kSnapshotStates].localAddress = { ipv4, ipv6 };
    }
  }

  // TODO(joyeecheung): consider exposing this if custom DNS resolvers
  // end up being useful for snapshot users.
  [kSerializeResolver]() {
    this._handle = null;  // We'll restore it during deserialization.
    addDeserializeCallback(function deserializeResolver(resolver) {
      resolver[kDeserializeResolver]();
    }, this);
  }

  [kDeserializeResolver]() {
    const { timeout, tries, localAddress, servers } = this[kSnapshotStates];
    this[kInitializeHandle](timeout, tries);
    if (localAddress) {
      const { ipv4, ipv6 } = localAddress;
      this._handle.setLocalAddress(ipv4, ipv6);
    }
    if (servers) {
      this[kSetServersInternal](servers, servers);
    }
  }
}

let defaultResolver;
let dnsOrder;

function initializeDns() {
  const orderFromCLI = getOptionValue('--dns-result-order');
  if (!orderFromCLI) {
    dnsOrder ??= 'verbatim';
  } else {
    // Allow the deserialized application to override order from CLI.
    validateOneOf(orderFromCLI, '--dns-result-order', ['verbatim', 'ipv4first', 'ipv6first']);
    dnsOrder = orderFromCLI;
  }

  if (!isBuildingSnapshot()) {
    return;
  }

  addSerializeCallback(() => {
    defaultResolver?.[kSerializeResolver]();
  });
}

const resolverKeys = [
  'getServers',
  'resolve',
  'resolve4',
  'resolve6',
  'resolveAny',
  'resolveCaa',
  'resolveCname',
  'resolveMx',
  'resolveNaptr',
  'resolveNs',
  'resolvePtr',
  'resolveSoa',
  'resolveSrv',
  'resolveTlsa',
  'resolveTxt',
  'reverse',
];

function getDefaultResolver() {
  // We do this here instead of pre-execution so that the default resolver is
  // only ever created when the user loads any dns module.
  if (defaultResolver === undefined) {
    defaultResolver = new ResolverBase();
  }
  return defaultResolver;
}

function setDefaultResolver(resolver) {
  defaultResolver = resolver;
}

function bindDefaultResolver(target, source) {
  const defaultResolver = getDefaultResolver();
  ArrayPrototypeForEach(resolverKeys, (key) => {
    target[key] = FunctionPrototypeBind(source[key], defaultResolver);
  });
}

function validateHints(hints) {
  const { AI_ADDRCONFIG, AI_ALL, AI_V4MAPPED } = lazyBinding();
  if ((hints & ~(AI_ADDRCONFIG | AI_ALL | AI_V4MAPPED)) !== 0) {
    throw new ERR_INVALID_ARG_VALUE('hints', hints);
  }
}

let invalidHostnameWarningEmitted = false;
function emitInvalidHostnameWarning(hostname) {
  if (!invalidHostnameWarningEmitted) {
    process.emitWarning(
      `The provided hostname "${hostname}" is not a valid ` +
      'hostname, and is supported in the dns module solely for compatibility.',
      'DeprecationWarning',
      'DEP0118',
    );
    invalidHostnameWarningEmitted = true;
  }
}

function setDefaultResultOrder(value) {
  validateOneOf(value, 'dnsOrder', ['verbatim', 'ipv4first', 'ipv6first']);
  dnsOrder = value;
}

function getDefaultResultOrder() {
  return dnsOrder;
}

function createResolverClass(resolver) {
  const resolveMap = { __proto__: null };

  class Resolver extends ResolverBase {}

  Resolver.prototype.resolveAny = resolveMap.ANY = resolver('queryAny');
  Resolver.prototype.resolve4 = resolveMap.A = resolver('queryA');
  Resolver.prototype.resolve6 = resolveMap.AAAA = resolver('queryAaaa');
  Resolver.prototype.resolveCaa = resolveMap.CAA = resolver('queryCaa');
  Resolver.prototype.resolveCname = resolveMap.CNAME = resolver('queryCname');
  Resolver.prototype.resolveMx = resolveMap.MX = resolver('queryMx');
  Resolver.prototype.resolveNs = resolveMap.NS = resolver('queryNs');
  Resolver.prototype.resolveTlsa = resolveMap.TLSA = resolver('queryTlsa');
  Resolver.prototype.resolveTxt = resolveMap.TXT = resolver('queryTxt');
  Resolver.prototype.resolveSrv = resolveMap.SRV = resolver('querySrv');
  Resolver.prototype.resolvePtr = resolveMap.PTR = resolver('queryPtr');
  Resolver.prototype.resolveNaptr = resolveMap.NAPTR = resolver('queryNaptr');
  Resolver.prototype.resolveSoa = resolveMap.SOA = resolver('querySoa');
  Resolver.prototype.reverse = resolver('getHostByAddr');

  return {
    resolveMap,
    Resolver,
  };
}

// ERROR CODES
const errorCodes = {
  NODATA: 'ENODATA',
  FORMERR: 'EFORMERR',
  SERVFAIL: 'ESERVFAIL',
  NOTFOUND: 'ENOTFOUND',
  NOTIMP: 'ENOTIMP',
  REFUSED: 'EREFUSED',
  BADQUERY: 'EBADQUERY',
  BADNAME: 'EBADNAME',
  BADFAMILY: 'EBADFAMILY',
  BADRESP: 'EBADRESP',
  CONNREFUSED: 'ECONNREFUSED',
  TIMEOUT: 'ETIMEOUT',
  EOF: 'EOF',
  FILE: 'EFILE',
  NOMEM: 'ENOMEM',
  DESTRUCTION: 'EDESTRUCTION',
  BADSTR: 'EBADSTR',
  BADFLAGS: 'EBADFLAGS',
  NONAME: 'ENONAME',
  BADHINTS: 'EBADHINTS',
  NOTINITIALIZED: 'ENOTINITIALIZED',
  LOADIPHLPAPI: 'ELOADIPHLPAPI',
  ADDRGETNETWORKPARAMS: 'EADDRGETNETWORKPARAMS',
  CANCELLED: 'ECANCELLED',
};

module.exports = {
  bindDefaultResolver,
  getDefaultResolver,
  setDefaultResolver,
  validateHints,
  validateTimeout,
  validateTries,
  emitInvalidHostnameWarning,
  getDefaultResultOrder,
  setDefaultResultOrder,
  errorCodes,
  createResolverClass,
  initializeDns,
};
