// Copyright 2018-2024 the Deno authors. All rights reserved. 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.

// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials

import { getOptionValue } from "ext:deno_node/internal/options.ts";
import { emitWarning } from "node:process";
import {
  AI_ADDRCONFIG,
  AI_ALL,
  AI_V4MAPPED,
} from "ext:deno_node/internal_binding/ares.ts";
import {
  ChannelWrap,
  strerror,
} from "ext:deno_node/internal_binding/cares_wrap.ts";
import {
  ERR_DNS_SET_SERVERS_FAILED,
  ERR_INVALID_ARG_VALUE,
  ERR_INVALID_IP_ADDRESS,
} from "ext:deno_node/internal/errors.ts";
import type { ErrnoException } from "ext:deno_node/internal/errors.ts";
import {
  validateArray,
  validateInt32,
  validateOneOf,
  validateString,
} from "ext:deno_node/internal/validators.mjs";
import { isIP } from "ext:deno_node/internal/net.ts";

export interface LookupOptions {
  family?: number | undefined;
  hints?: number | undefined;
  all?: boolean | undefined;
  verbatim?: boolean | undefined;
}

export interface LookupOneOptions extends LookupOptions {
  all?: false | undefined;
}

export interface LookupAllOptions extends LookupOptions {
  all: true;
}

export interface LookupAddress {
  address: string | null;
  family: number;
}

export function isLookupOptions(
  options: unknown,
): options is LookupOptions | undefined {
  return typeof options === "object" || typeof options === "undefined";
}

export function isLookupCallback(
  options: unknown,
): options is (...args: unknown[]) => void {
  return typeof options === "function";
}

export function isFamily(options: unknown): options is number {
  return typeof options === "number";
}

export interface ResolveOptions {
  ttl?: boolean;
}

export interface ResolveWithTtlOptions extends ResolveOptions {
  ttl: true;
}

export interface RecordWithTtl {
  address: string;
  ttl: number;
}

export interface AnyARecord extends RecordWithTtl {
  type: "A";
}

export interface AnyAaaaRecord extends RecordWithTtl {
  type: "AAAA";
}

export interface CaaRecord {
  critial: number;
  issue?: string | undefined;
  issuewild?: string | undefined;
  iodef?: string | undefined;
  contactemail?: string | undefined;
  contactphone?: string | undefined;
}

export interface MxRecord {
  priority: number;
  exchange: string;
}

export interface AnyMxRecord extends MxRecord {
  type: "MX";
}

export interface NaptrRecord {
  flags: string;
  service: string;
  regexp: string;
  replacement: string;
  order: number;
  preference: number;
}

export interface AnyNaptrRecord extends NaptrRecord {
  type: "NAPTR";
}

export interface SoaRecord {
  nsname: string;
  hostmaster: string;
  serial: number;
  refresh: number;
  retry: number;
  expire: number;
  minttl: number;
}

export interface AnySoaRecord extends SoaRecord {
  type: "SOA";
}

export interface SrvRecord {
  priority: number;
  weight: number;
  port: number;
  name: string;
}

export interface AnySrvRecord extends SrvRecord {
  type: "SRV";
}

export interface AnyTxtRecord {
  type: "TXT";
  entries: string[];
}

export interface AnyNsRecord {
  type: "NS";
  value: string;
}

export interface AnyPtrRecord {
  type: "PTR";
  value: string;
}

export interface AnyCnameRecord {
  type: "CNAME";
  value: string;
}

export type AnyRecord =
  | AnyARecord
  | AnyAaaaRecord
  | AnyCnameRecord
  | AnyMxRecord
  | AnyNaptrRecord
  | AnyNsRecord
  | AnyPtrRecord
  | AnySoaRecord
  | AnySrvRecord
  | AnyTxtRecord;

export type Records =
  | string[]
  | AnyRecord[]
  | MxRecord[]
  | NaptrRecord[]
  | SoaRecord
  | SrvRecord[]
  | string[];

export type ResolveCallback = (
  err: ErrnoException | null,
  addresses: Records,
) => void;

export function isResolveCallback(
  callback: unknown,
): callback is ResolveCallback {
  return typeof callback === "function";
}

const IANA_DNS_PORT = 53;
const IPv6RE = /^\[([^[\]]*)\]/;
const addrSplitRE = /(^.+?)(?::(\d+))?$/;

export function validateTimeout(options?: { timeout?: number }) {
  const { timeout = -1 } = { ...options };
  validateInt32(timeout, "options.timeout", -1, 2 ** 31 - 1);
  return timeout;
}

export function validateTries(options?: { tries?: number }) {
  const { tries = 4 } = { ...options };
  validateInt32(tries, "options.tries", 1, 2 ** 31 - 1);
  return tries;
}

export interface ResolverOptions {
  timeout?: number | undefined;
  /**
   * @default 4
   */
  tries?: number;
}

/**
 * An independent resolver for DNS requests.
 *
 * Creating a new resolver uses the default server settings. Setting
 * the servers used for a resolver using `resolver.setServers()` does not affect
 * other resolvers:
 *
 * ```js
 * const { Resolver } = require('dns');
 * const resolver = new Resolver();
 * resolver.setServers(['4.4.4.4']);
 *
 * // This request will use the server at 4.4.4.4, independent of global settings.
 * resolver.resolve4('example.org', (err, addresses) => {
 *   // ...
 * });
 * ```
 *
 * The following methods from the `dns` module are available:
 *
 * - `resolver.getServers()`
 * - `resolver.resolve()`
 * - `resolver.resolve4()`
 * - `resolver.resolve6()`
 * - `resolver.resolveAny()`
 * - `resolver.resolveCaa()`
 * - `resolver.resolveCname()`
 * - `resolver.resolveMx()`
 * - `resolver.resolveNaptr()`
 * - `resolver.resolveNs()`
 * - `resolver.resolvePtr()`
 * - `resolver.resolveSoa()`
 * - `resolver.resolveSrv()`
 * - `resolver.resolveTxt()`
 * - `resolver.reverse()`
 * - `resolver.setServers()`
 */
export class Resolver {
  _handle!: ChannelWrap;

  constructor(options?: ResolverOptions) {
    const timeout = validateTimeout(options);
    const tries = validateTries(options);
    this._handle = new ChannelWrap(timeout, tries);
  }

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

  getServers(): string[] {
    return this._handle.getServers().map((val: [string, number]) => {
      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: ReadonlyArray<string>) {
    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 orig = this._handle.getServers();
    const newSet: [number, string, number][] = [];

    servers.forEach((serv, index) => {
      validateString(serv, `servers[${index}]`);
      let ipVersion = isIP(serv);

      if (ipVersion !== 0) {
        return newSet.push([ipVersion, serv, IANA_DNS_PORT]);
      }

      const match = serv.match(IPv6RE);

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

        if (ipVersion !== 0) {
          const port = Number.parseInt(serv.replace(addrSplitRE, "$2")) ||
            IANA_DNS_PORT;

          return newSet.push([ipVersion, match[1], port]);
        }
      }

      // addr::port
      const addrSplitMatch = serv.match(addrSplitRE);

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

        ipVersion = isIP(hostIP);

        if (ipVersion !== 0) {
          return newSet.push([ipVersion, hostIP, Number.parseInt(port)]);
        }
      }

      throw new ERR_INVALID_IP_ADDRESS(serv);
    });

    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.join(","));
      const err = strerror(errorNumber);

      throw new ERR_DNS_SET_SERVERS_FAILED(err, servers.toString());
    }
  }

  /**
   * The resolver instance will send its requests from the specified IP address.
   * This allows programs to specify outbound interfaces when used on multi-homed
   * systems.
   *
   * If a v4 or v6 address is not specified, it is set to the default, and the
   * operating system will choose a local address automatically.
   *
   * The resolver will use the v4 local address when making requests to IPv4 DNS
   * servers, and the v6 local address when making requests to IPv6 DNS servers.
   * The `rrtype` of resolution requests has no impact on the local address used.
   *
   * @param [ipv4='0.0.0.0'] A string representation of an IPv4 address.
   * @param [ipv6='::0'] A string representation of an IPv6 address.
   */
  setLocalAddress(ipv4: string, ipv6?: string) {
    validateString(ipv4, "ipv4");

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

    this._handle.setLocalAddress(ipv4, ipv6);
  }
}

let defaultResolver = new Resolver();

export function getDefaultResolver(): Resolver {
  return defaultResolver;
}

export function setDefaultResolver<T extends Resolver>(resolver: T) {
  defaultResolver = resolver;
}

export function validateHints(hints: number) {
  if ((hints & ~(AI_ADDRCONFIG | AI_ALL | AI_V4MAPPED)) !== 0) {
    throw new ERR_INVALID_ARG_VALUE("hints", hints, "is invalid");
  }
}

let invalidHostnameWarningEmitted = false;

export function emitInvalidHostnameWarning(hostname: string) {
  if (invalidHostnameWarningEmitted) {
    return;
  }

  invalidHostnameWarningEmitted = true;

  emitWarning(
    `The provided hostname "${hostname}" is not a valid ` +
      "hostname, and is supported in the dns module solely for compatibility.",
    "DeprecationWarning",
    "DEP0118",
  );
}

let dnsOrder = getOptionValue("--dns-result-order") || "ipv4first";

export function getDefaultVerbatim() {
  switch (dnsOrder) {
    case "verbatim": {
      return true;
    }
    case "ipv4first": {
      return false;
    }
    default: {
      return false;
    }
  }
}

/**
 * Set the default value of `verbatim` in `lookup` and `dnsPromises.lookup()`.
 * The value could be:
 *
 * - `ipv4first`: sets default `verbatim` `false`.
 * - `verbatim`: sets default `verbatim` `true`.
 *
 * The default is `ipv4first` and `setDefaultResultOrder` have higher
 * priority than `--dns-result-order`. When using `worker threads`,
 * `setDefaultResultOrder` from the main thread won't affect the default
 * dns orders in workers.
 *
 * @param order must be `'ipv4first'` or `'verbatim'`.
 */
export function setDefaultResultOrder(order: "ipv4first" | "verbatim") {
  validateOneOf(order, "dnsOrder", ["verbatim", "ipv4first"]);
  dnsOrder = order;
}

export function defaultResolverSetServers(servers: string[]) {
  const resolver = new Resolver();

  resolver.setServers(servers);
  setDefaultResolver(resolver);
}