// Copyright 2018-2026 the Deno authors. MIT license.
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

(function () {
const { core, primordials } = __bootstrap;
const {
  ArrayPrototypeMap,
  FunctionPrototypeBind,
  ObjectCreate,
  ObjectDefineProperty,
  Promise,
  ReflectApply,
} = primordials;
const {
  validateBoolean,
  validateNumber,
  validateOneOf,
  validatePort,
  validateString,
} = core.loadExtScript("ext:deno_node/internal/validators.mjs");
const { isIP } = core.loadExtScript("ext:deno_node/internal/net.ts");
const {
  dnsOrderToNumber,
  getDefaultDnsOrder,
  getDefaultResolver,
  isFamily,
  isLookupOptions,
  Resolver: CallbackResolver,
  validateHints,
  validDnsOrders,
} = core.loadExtScript("ext:deno_node/internal/dns/utils.ts");

const {
  dnsException,
  ERR_INVALID_ARG_TYPE,
  ERR_INVALID_ARG_VALUE,
  ERR_MISSING_ARGS,
  handleDnsError,
} = core.loadExtScript("ext:deno_node/internal/errors.ts");
const {
  default: cares,
  GetAddrInfoReqWrap,
  GetNameInfoReqWrap,
  QueryReqWrap,
} = core.loadExtScript("ext:deno_node/internal_binding/cares_wrap.ts");
const { domainToASCII } = core.loadExtScript(
  "ext:deno_node/internal/idna.ts",
);

function onlookup(
  this: GetAddrInfoReqWrap,
  err: number | null,
  addresses: string[],
) {
  if (err) {
    this.reject(dnsException(err, "getaddrinfo", this.hostname));
    return;
  }

  const family = this.family || isIP(addresses[0]);
  this.resolve({ address: addresses[0], family });
}

function onlookupall(
  this: GetAddrInfoReqWrap,
  err: number | null,
  addresses: string[],
) {
  if (err) {
    this.reject(dnsException(err, "getaddrinfo", this.hostname));

    return;
  }

  const family = this.family;
  const parsedAddresses = [];

  for (let i = 0; i < addresses.length; i++) {
    const address = addresses[i];
    parsedAddresses[i] = {
      address,
      family: family ? family : isIP(address),
    };
  }

  this.resolve(parsedAddresses);
}

function createLookupPromise(
  family: number,
  hostname: string,
  all: boolean,
  hints: number,
  dnsOrder: string,
): Promise<void | LookupAddress | LookupAddress[]> {
  return new Promise((resolve, reject) => {
    if (!hostname) {
      if (all) {
        resolve([]);
      } else {
        resolve({ address: null, family: family === 6 ? 6 : 4 });
      }
      return;
    }

    const matchedFamily = isIP(hostname);

    if (matchedFamily !== 0) {
      const result = { address: hostname, family: matchedFamily };
      resolve(all ? [result] : result);

      return;
    }

    const req = new GetAddrInfoReqWrap();

    req.family = family;
    req.hostname = hostname;
    req.oncomplete = all ? onlookupall : onlookup;
    req.resolve = resolve;
    req.reject = reject;

    const err = cares.getaddrinfo(
      req,
      domainToASCII(hostname),
      family,
      hints,
      dnsOrderToNumber(dnsOrder),
    );

    if (err) {
      reject(dnsException(err, "getaddrinfo", hostname));
    }
  });
}

const validFamilies = [0, 4, 6];

function lookup(
  hostname: string,
  family: number,
): Promise<void | LookupAddress | LookupAddress[]>;
function lookup(
  hostname: string,
  options: LookupOneOptions,
): Promise<void | LookupAddress | LookupAddress[]>;
function lookup(
  hostname: string,
  options: LookupAllOptions,
): Promise<void | LookupAddress | LookupAddress[]>;
function lookup(
  hostname: string,
  options: LookupOptions,
): Promise<void | LookupAddress | LookupAddress[]>;
function lookup(
  hostname: string,
  options: unknown,
): Promise<void | LookupAddress | LookupAddress[]> {
  let hints = 0;
  let family = 0;
  let all = false;
  let dnsOrder = getDefaultDnsOrder();

  // Parse arguments
  if (hostname) {
    validateString(hostname, "hostname");
  }

  if (isFamily(options)) {
    validateOneOf(options, "family", validFamilies);
    family = options;
  } else if (!isLookupOptions(options)) {
    throw new ERR_INVALID_ARG_TYPE("options", ["integer", "object"], options);
  } else {
    if (options?.hints != null) {
      validateNumber(options.hints, "options.hints");
      hints = options.hints >>> 0;
      validateHints(hints);
    }

    if (options?.family != null) {
      // Accept both numeric (0, 4, 6) and string ('IPv4', 'IPv6') family values
      // to match Node.js behavior
      switch (options.family) {
        case "IPv4":
          family = 4;
          break;
        case "IPv6":
          family = 6;
          break;
        default:
          validateOneOf(options.family, "options.family", validFamilies);
          family = options.family;
      }
    }

    if (options?.all != null) {
      validateBoolean(options.all, "options.all");
      all = options.all;
    }

    if (options?.verbatim != null) {
      validateBoolean(options.verbatim, "options.verbatim");
      dnsOrder = options.verbatim ? "verbatim" : "ipv4first";
    }

    if ((options as Record<string, unknown>)?.order != null) {
      validateOneOf(
        (options as Record<string, unknown>).order,
        "options.order",
        validDnsOrders,
      );
      dnsOrder = (options as Record<string, unknown>).order as string;
    }
  }

  return createLookupPromise(family, hostname, all, hints, dnsOrder);
}

function onresolve(
  this: QueryReqWrap,
  err: number,
  records: Records,
  ttls?: number[],
) {
  if (err) {
    this.reject(dnsException(err, this.bindingName, this.hostname));

    return;
  }

  const parsedRecords = ttls && this.ttl
    ? ArrayPrototypeMap(
      records as string[],
      (address: string, index: number) => ({
        address,
        ttl: ttls[index],
      }),
    )
    : records;

  this.resolve(parsedRecords);
}

function onlookupservice(
  this: GetNameInfoReqWrap,
  err: Error | null,
  hostname?: string,
  service?: string,
) {
  if (err) {
    this.reject(handleDnsError(err, "getnameinfo", this.address));
    return;
  }

  this.resolve({ hostname: hostname!, service: service! });
}

function createLookupServicePromise(address: string, port: number) {
  return new Promise((resolve, reject) => {
    const req = new GetNameInfoReqWrap();

    req.address = address;
    req.port = port;
    req.oncomplete = onlookupservice;
    req.resolve = resolve;
    req.reject = reject;

    const errCode = cares.getnameinfo(req, address, port);

    if (errCode) {
      reject(dnsException(errCode, "getnameinfo", address));
    }
  });
}

function lookupService(address: string, port: number) {
  if (arguments.length !== 2) {
    throw new ERR_MISSING_ARGS("address", "port");
  }

  if (isIP(address) === 0) {
    throw new ERR_INVALID_ARG_VALUE("address", address);
  }

  port = validatePort(port);

  return createLookupServicePromise(address, port);
}

function createResolverPromise(
  resolver: Resolver,
  bindingName: string,
  hostname: string,
  ttl: boolean,
) {
  return new Promise((resolve, reject) => {
    const req = new QueryReqWrap();

    req.bindingName = bindingName;
    req.hostname = hostname;
    req.oncomplete = onresolve;
    req.resolve = resolve;
    req.reject = reject;
    req.ttl = ttl;

    const err = resolver._handle[bindingName](req, domainToASCII(hostname));

    if (err) {
      reject(dnsException(err, bindingName, hostname));
    }
  });
}

function resolver(bindingName: string) {
  function query(
    this: Resolver,
    name: string,
    options?: unknown,
  ) {
    validateString(name, "name");

    const ttl = !!(options && (options as ResolveOptions).ttl);

    return createResolverPromise(this, bindingName, name, ttl);
  }

  ObjectDefineProperty(query, "name", {
    __proto__: null,
    value: bindingName,
  });

  return query;
}

const resolveMap = ObjectCreate(null);

class Resolver extends CallbackResolver {
  // deno-lint-ignore no-explicit-any
  [resolveMethod: string]: any;
}

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.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");
Resolver.prototype.resolve = _resolve;

function _resolve(
  this: Resolver,
  hostname: string,
  rrtype?: string,
) {
  let resolver;

  if (typeof hostname !== "string") {
    throw new ERR_INVALID_ARG_TYPE("name", "string", hostname);
  }

  if (rrtype !== undefined) {
    validateString(rrtype, "rrtype");

    resolver = resolveMap[rrtype];

    if (typeof resolver !== "function") {
      throw new ERR_INVALID_ARG_VALUE("rrtype", rrtype);
    }
  } else {
    resolver = resolveMap.A;
  }

  return ReflectApply(resolver, this, [hostname]);
}

// The Node implementation uses `bindDefaultResolver` to set the follow methods
// on `module.exports` bound to the current `defaultResolver`. We don't have
// the same ability in ESM but can simulate this (at some cost) by explicitly
// exporting these methods which dynamically bind to the default resolver when
// called.

function getServers(): string[] {
  return FunctionPrototypeBind(
    Resolver.prototype.getServers,
    getDefaultResolver(),
  )();
}

function resolveAny(
  hostname: string,
) {
  return FunctionPrototypeBind(
    Resolver.prototype.resolveAny,
    getDefaultResolver() as Resolver,
  )(
    hostname,
  );
}

function resolve4(
  hostname: string,
): Promise<void>;
function resolve4(
  hostname: string,
  options: ResolveWithTtlOptions,
): Promise<void>;
function resolve4(
  hostname: string,
  options: ResolveOptions,
): Promise<void>;
function resolve4(hostname: string, options?: unknown) {
  return FunctionPrototypeBind(
    Resolver.prototype.resolve4,
    getDefaultResolver() as Resolver,
  )(
    hostname,
    options,
  );
}

function resolve6(hostname: string): Promise<void>;
function resolve6(
  hostname: string,
  options: ResolveWithTtlOptions,
): Promise<void>;
function resolve6(
  hostname: string,
  options: ResolveOptions,
): Promise<void>;
function resolve6(hostname: string, options?: unknown) {
  return FunctionPrototypeBind(
    Resolver.prototype.resolve6,
    getDefaultResolver() as Resolver,
  )(
    hostname,
    options,
  );
}

function resolveCaa(
  hostname: string,
) {
  return FunctionPrototypeBind(
    Resolver.prototype.resolveCaa,
    getDefaultResolver() as Resolver,
  )(
    hostname,
  );
}

function resolveCname(
  hostname: string,
) {
  return FunctionPrototypeBind(
    Resolver.prototype.resolveCname,
    getDefaultResolver() as Resolver,
  )(
    hostname,
  );
}

function resolveMx(
  hostname: string,
) {
  return FunctionPrototypeBind(
    Resolver.prototype.resolveMx,
    getDefaultResolver() as Resolver,
  )(
    hostname,
  );
}

function resolveNs(hostname: string) {
  return FunctionPrototypeBind(
    Resolver.prototype.resolveNs,
    getDefaultResolver() as Resolver,
  )(
    hostname,
  );
}

function resolveTxt(hostname: string) {
  return FunctionPrototypeBind(
    Resolver.prototype.resolveTxt,
    getDefaultResolver() as Resolver,
  )(
    hostname,
  );
}

function resolveSrv(hostname: string) {
  return FunctionPrototypeBind(
    Resolver.prototype.resolveSrv,
    getDefaultResolver() as Resolver,
  )(
    hostname,
  );
}

function resolvePtr(hostname: string) {
  return FunctionPrototypeBind(
    Resolver.prototype.resolvePtr,
    getDefaultResolver() as Resolver,
  )(
    hostname,
  );
}

function resolveNaptr(hostname: string) {
  return FunctionPrototypeBind(
    Resolver.prototype.resolveNaptr,
    getDefaultResolver() as Resolver,
  )(
    hostname,
  );
}

function resolveSoa(hostname: string) {
  return FunctionPrototypeBind(
    Resolver.prototype.resolveSoa,
    getDefaultResolver() as Resolver,
  )(
    hostname,
  );
}

function reverse(ip: string) {
  return FunctionPrototypeBind(
    Resolver.prototype.reverse,
    getDefaultResolver() as Resolver,
  )(
    ip,
  );
}

function resolve(
  hostname: string,
): Promise<void>;
function resolve(
  hostname: string,
  rrtype: "A",
): Promise<void>;
function resolve(
  hostname: string,
  rrtype: "AAAA",
): Promise<void>;
function resolve(
  hostname: string,
  rrtype: "ANY",
): Promise<void>;
function resolve(
  hostname: string,
  rrtype: "CNAME",
): Promise<void>;
function resolve(
  hostname: string,
  rrtype: "MX",
): Promise<void>;
function resolve(
  hostname: string,
  rrtype: "NAPTR",
): Promise<void>;
function resolve(
  hostname: string,
  rrtype: "NS",
): Promise<void>;
function resolve(
  hostname: string,
  rrtype: "PTR",
): Promise<void>;
function resolve(
  hostname: string,
  rrtype: "SOA",
): Promise<void>;
function resolve(
  hostname: string,
  rrtype: "SRV",
): Promise<void>;
function resolve(
  hostname: string,
  rrtype: "TXT",
): Promise<void>;
function resolve(
  hostname: string,
  rrtype: string,
): Promise<void>;
function resolve(hostname: string, rrtype?: string) {
  return FunctionPrototypeBind(
    Resolver.prototype.resolve,
    getDefaultResolver() as Resolver,
  )(
    hostname,
    rrtype,
  );
}

return {
  default: {
    lookup,
    lookupService,
    Resolver,
    getDefaultResultOrder: getDefaultDnsOrder,
    getServers,
    resolveAny,
    resolve4,
    resolve6,
    resolveCaa,
    resolveCname,
    resolveMx,
    resolveNs,
    resolveTxt,
    resolveSrv,
    resolvePtr,
    resolveNaptr,
    resolveSoa,
    resolve,
    reverse,
  },
  Resolver,
  getDefaultResultOrder: getDefaultDnsOrder,
  lookup,
  lookupService,
  getServers,
  resolveAny,
  resolve4,
  resolve6,
  resolveCaa,
  resolveCname,
  resolveMx,
  resolveNs,
  resolveTxt,
  resolveSrv,
  resolvePtr,
  resolveNaptr,
  resolveSoa,
  resolve,
  reverse,
};
})();
