
module Resolver = Dns_resolver_mirage.Make(Tcpip_stack_socket.V4V6)

open Lwt.Infix

let pp_val ppf f =
  let open Metrics in
  match value f with
  | V (String, s) -> Fmt.pf ppf "%S" s
  | V (Int, i) -> Fmt.pf ppf "%d" i
  | V (Int32, i32) -> Fmt.pf ppf "%ld" i32
  | V (Int64, i64) -> Fmt.pf ppf "%Ld" i64
  | V (Uint, u) -> Fmt.pf ppf "%u" u
  | V (Uint32, u32) -> Fmt.pf ppf "%lu" u32
  | V (Uint64, u64) -> Fmt.pf ppf "%Lu" u64
  | _ -> pp_value ppf f

let print_resolver_stats () =
  let map = Metrics.get_cache () in
  let dns_resolver_src =
    List.find (fun src -> Metrics.Src.name src = "dns-resolver") (Metrics.Src.list ())
  in
  let dns_resolver_metrics =
    match Metrics.SM.find_opt dns_resolver_src map with
    | None ->
      print_endline "no dns-resolver found";
      []
    | Some ms ->
      List.concat_map (fun (_tags, data) -> Metrics.Data.fields data) ms
  in
  List.iter (fun field ->
      Logs.app (fun m -> m "%s %a" (Metrics.key field) pp_val field))
    dns_resolver_metrics;
  exit 130

let main dnssec qname_min opportunistic =
  Mirage_crypto_rng_unix.use_default ();
  let reporter = Metrics.cache_reporter () in
  Metrics.set_reporter reporter;
  Metrics.enable_all ();
  Udpv4v6_socket.connect ~ipv4_only:true ~ipv6_only:false Ipaddr.V4.Prefix.global None >>= fun udp ->
  Tcpv4v6_socket.connect ~ipv4_only:true ~ipv6_only:false Ipaddr.V4.Prefix.global None >>= fun tcp ->
  Tcpip_stack_socket.V4V6.connect udp tcp >>= fun stack ->
  let resolver =
    let primary_t =
      (* setup DNS server state: *)
      Dns_server.Primary.create ~rng:Mirage_crypto_rng.generate Dns_trie.empty
    in
    let features =
      (if dnssec then [ `Dnssec ] else []) @
      (if qname_min then [ `Qname_minimisation ] else []) @
      (if opportunistic then [ `Opportunistic_tls_authoritative ] else [])
    in
    Dns_resolver.create features ~ip_protocol:`Ipv4_only
      (Mirage_mtime.elapsed_ns ()) Mirage_crypto_rng.generate primary_t
  in
  let _resolver = Resolver.resolver ~port:53530 stack resolver in
  let _ : Sys.signal_behavior =
    Sys.signal Sys.sigint
      (Signal_handle
         (fun _ -> print_resolver_stats ()))
  in
  Tcpip_stack_socket.V4V6.listen stack >|= fun () ->
  Ok ()

let jump () dnssec qname_min opportunistic =
  Lwt_main.run (main dnssec qname_min opportunistic)

open Cmdliner

let dnssec =
  let doc =
    Arg.info ~doc:"Validate DNS replies and cache DNSSEC data." [ "dnssec" ]
  in
  Arg.(value & flag doc)

let qname_minimisation =
  let doc =
    Arg.info ~doc:"Use qname minimisation (RFC 9156)." [ "qname-minimisation" ]
  in
  Arg.(value & flag doc)

let opportunistic_tls =
  let doc =
    Arg.info ~doc:"Use opportunistic TLS from recursive resolver to authoriative (RFC 9539)."
      [ "opportunistic-tls-authoritative" ]
  in
  Arg.(value & flag doc)

let cmd =
  let term =
    Term.(term_result (const jump $ Dns_cli.setup_log $ dnssec $ qname_minimisation $ opportunistic_tls))
  and info = Cmd.info "resolver" ~version:"10.2.2"
  in
  Cmd.v info term

let () = exit (Cmd.eval cmd)
