(* TODO ideally there'd be something like mirage-flow-lwt that didn't depend
        on lwt and a ton of other things, and still provided [map]
        and [connect] and so on. leaving this stuff here for now until a
        better solution presents itself. *)

val default_resolver_hostname : [`host] Domain_name.t

val default_resolvers : Ipaddr.t list
(** [default_resolver] is a list of IPv6 and IPv4 address of the default
    resolver. Currently it is the IP address of the UncensoredDNS.org
    anycast service. *)

module type S = sig
  type context
  (** A context is a network connection initialized by {!connect} *)

  type +'a io
  (** [io] is the type of an effect. ['err] is a polymorphic variant. *)

  type io_addr
  (** An address for a given context type, usually this will consist of
      IP address + a TCP/IP or UDP/IP port number, but for some context types
      it can carry additional information for purposes of cryptographic
      verification. *)

  type stack
  (** A stack with which to connect. *)

  type t
  (** The abstract state of a DNS client. *)

  val create : ?nameservers:(Dns.proto * io_addr list) -> timeout:int64 ->
    stack -> t
  (** [create ~nameservers ~timeout stack] creates the state record of
      the DNS client. We use [timeout] (ns) as a cumulative time budget for
      connect and request timeouts. *)

  val nameservers : t -> Dns.proto * io_addr list
  (** The address of a nameservers that is supposed to work with
      the underlying context, can be used if the user does not want to
      bother with configuring their own.*)

  val rng : int -> string
  (** [rng t] is a random number generator. *)

  val clock : unit -> int64
  (** [clock t] is the monotonic clock. *)

  val connect : t -> (Dns.proto * context, [> `Msg of string ]) result io
  (** [connect t] is a new connection ([context]) to [t], or an error. *)

  val send_recv : context -> string -> (string, [> `Msg of string ]) result io
  (** [send_recv context buffer] sends [buffer] to the [context] upstream, and
      then reads a buffer. *)

  val close : context -> unit io
  (** [close context] closes the [context], freeing up resources. *)

  val bind : 'a io -> ('a -> 'b io) -> 'b io
  (** a.k.a. [>>=] *)

  val lift : 'a -> 'a io
end

module Make : functor (T : S) ->
sig

  type t
  (** The abstract type of a DNS client. *)

  val transport : t -> T.t
  (** [transport t] is the transport of [t]. *)

  val create : ?cache_size:int ->
    ?edns:[ `None | `Auto | `Manual of Dns.Edns.t ] ->
    ?nameservers:(Dns.proto * T.io_addr list) -> ?timeout:int64 ->
    T.stack -> t
  (** [create ~cache_size ~edns ~nameservers ~timeout stack] creates the state
      of the DNS client. We use [timeout] (ns, default 5s) as a time budget for
      connect and request timeouts. To specify a timeout, use
      [create ~timeout:(Duration.of_sec 3)]. Whether or not to use
      {{:https://tools.ietf.org/html/rfc6891}EDNS} in queries is controlled
      by [~edns] (defaults to [`None]): if [None], no EDNS will be present,
      [`Auto] adds TCP Keepalive if protocol is TCP, [`Manual edns] adds the
      EDNS data specified. *)

  val nameservers : t -> Dns.proto * T.io_addr list
  (** [nameservers state] returns the list of nameservers to be used. *)

  val getaddrinfo : t -> 'response Dns.Rr_map.key ->
    'a Domain_name.t ->
    ('response, [> `Msg of string ]) result T.io
  (** [getaddrinfo state query_type name] is the
      [query_type]-dependent response regarding [name], or
      an [Error _] message. See {!Dns_client.query_state} for more information
      about the result types. *)

  val gethostbyname : t -> [ `host ] Domain_name.t ->
    (Ipaddr.V4.t, [> `Msg of string ]) result T.io
  (** [gethostbyname state hostname] is the IPv4 address of
      [hostname] resolved via the [state] specified.
      If the query fails, or if the [domain] does not have any IPv4 addresses,
      an [Error _] message is returned. Any extraneous IPv4 addresses are
      ignored. For an example of using this API, see [unix/ohost.ml] in the
      distribution of this package. *)

  val gethostbyname6 : t -> [ `host ] Domain_name.t ->
    (Ipaddr.V6.t, [> `Msg of string ]) result T.io
  (** [gethostbyname6 state hostname] is the IPv6 address of
      [hostname] resolved via the [state] specified.

      It is the IPv6 equivalent of {!gethostbyname}. *)

  val get_resource_record : t -> 'response Dns.Rr_map.key -> 'a Domain_name.t ->
    ('response,
     [> `Msg of string
     | `No_data of [ `raw ] Domain_name.t * Dns.Soa.t
     | `No_domain of [ `raw ] Domain_name.t * Dns.Soa.t ]) result T.io
  (** [get_resource_record state query_type name] resolves
      [query_type, name] via the [state] specified. The
      behaviour is equivalent to {!getaddrinfo}, apart from the error return
      value - [get_resource_record] distinguishes some errors, at the moment
      [No_data] if the [name] exists, but not the [query_type], and
      [No_domain] if the [name] does not exist. This allows clients to treat
      these error conditions explicitly. *)

  val get_raw_reply : t -> 'response Dns.Rr_map.key ->
    'a Domain_name.t ->
    (Dns.Packet.reply, [> `Partial | `Msg of string ]) result T.io
  (** [get_raw_reply state query_type name] resolves [query_type, name] via the
       [state] specified. The complete DNS reply is returned. CNAME records
       are not followed. This allows DNSSec to process the entire reply. *)
end

module Pure : sig
  (** The pure interface to the client part of uDns.

      Various helper modules to do with side effects are available from
      {!Dns_client_lwt}, {!Dns_client_unix} and so forth. *)

  type 'key query_state constraint 'key = 'a Dns.Rr_map.key
  (** [query_state] is parameterized over the query type, so the type of the
      representation of the answer depends on what the name server was asked to
      provide. See {!Dns.Rr_map.k} for a list of response types. The first
      element (the [int32]) in most of the tuples is the Time-To-Live (TTL)
      field returned from the server, which you can use to calculate when you
      should request fresh information in case you are writing a long-running
      application. *)

  val make_query :
    (int -> string) -> Dns.proto -> ?dnssec:bool ->
    [ `None | `Auto | `Manual of Dns.Edns.t ] ->
    'a Domain_name.t ->
    'query_type Dns.Rr_map.key ->
    string * 'query_type Dns.Rr_map.key query_state
  (** [make_query rng protocol name query_type] is [query, query_state]
      where [query] is the serialized DNS query to send to the name server,
      and [query_state] is the information required to validate the response. *)

  val parse_response : 'query_type Dns.Rr_map.key query_state -> string ->
    (Dns.Packet.reply, [ `Partial | `Msg of string]) result
  (** [parse_response query_state response] is the information contained in
      [response] parsed using [query_state] when the query was successful, or
      an [`Msg message] if the [response] did not match the [query_state]
      (or if the query failed).

      In a TCP usage context the [`Partial] means there are more bytes to be
      read in order to parse correctly. This can happen due to short reads or if
      the server (or something along the route) chunks its responses into
      multiple individual packets. In that case you should concatenate
      [response] and the next received data and call this function again.

      In a UDP usage context the [`Partial] means information was lost, due to
      an incomplete packet. *)

  val handle_response : 'query_type Dns.Rr_map.key query_state -> string ->
    ( [ `Data of 'query_type
      | `Partial
      | `No_data of [`raw] Domain_name.t * Dns.Soa.t
      | `No_domain of [`raw] Domain_name.t * Dns.Soa.t ],
      [`Msg of string]) result
  (** [handle_response query_state response] is the information contained in
      [response] parsed using [query_state] when the query was successful, or
      an [`Msg message] if the [response] did not match the [query_state]
      (or if the query failed).

      In a TCP usage context the [`Partial] means there are more bytes to be
      read in order to parse correctly. This can happen due to short reads or if
      the server (or something along the route) chunks its responses into
      multiple individual packets. In that case you should concatenate
      [response] and the next received data and call this function again.

      In a UDP usage context the [`Partial] means information was lost, due to
      an incomplete packet. *)

end
