(* (c) 2019 Hannes Mehnert, all rights reserved *)

(* goal is to check a given zonefile whether it is valid (and to-be-used
   by an authoritative NS - i.e. there must be a SOA record, TTL are good)
   if a NS/MX name is within the zone, it needs an address record
   the name of the file is taken as the domain name *)
open Dns

let ( let* ) = Result.bind

let load_zone zone =
  let* data = Bos.OS.File.read Fpath.(v zone) in
  let* rrs = Dns_zone.parse data in
  let domain = Domain_name.of_string_exn Fpath.(basename (v zone)) in
  let bad = Domain_name.Map.filter
      (fun name _ -> not (Domain_name.is_subdomain ~domain ~subdomain:name))
      rrs
  in
  if not (Domain_name.Map.is_empty bad) then
    Error (`Msg (Fmt.str "Entries of domain '%a' are not in its zone, won't handle this:@.%a"
                   Domain_name.pp domain Dns.Name_rr_map.pp bad))
  else
    Ok (Dns_trie.insert_map rrs Dns_trie.empty)

let jump _ zone old =
  let* trie = load_zone zone in
  let* () =
    Result.map_error
      (fun e -> `Msg (Fmt.to_to_string Dns_trie.pp_zone_check e))
      (Dns_trie.check trie)
  in
  Logs.app (fun m -> m "successfully checked zone") ;
  let zones =
    Dns_trie.fold Soa trie
      (fun name _ acc -> Domain_name.Set.add name acc)
      Domain_name.Set.empty
  in
  if Domain_name.Set.cardinal zones = 1 then
    let zone = Domain_name.Set.choose zones in
    let* zone_data = Dns_server.text zone trie in
    Logs.debug (fun m -> m "assembled zone data %s" zone_data) ;
    (match old with
     | None -> Ok ()
     | Some fn ->
       let* old = load_zone fn in
       match Dns_trie.lookup zone Soa trie, Dns_trie.lookup zone Soa old with
       | Ok fresh, Ok old when Soa.newer ~old fresh ->
         Logs.debug (fun m -> m "zone %a newer than old" Domain_name.pp zone) ;
         Ok ()
       | _ ->
         Error (`Msg "SOA comparison wrong"))
  else
    Error (`Msg "expected exactly one zone")

open Cmdliner

let newzone =
  let doc = "New zone file" in
  Arg.(required & pos 0 (some file) None & info [] ~doc ~docv:"ZONE")

let oldzone =
  let doc = "Old zone file" in
  Arg.(value & opt (some file) None & info [ "old" ] ~doc ~docv:"ZONE")

let cmd =
  let term =
    Term.(term_result (const jump $ Dns_cli.setup_log $ newzone $ oldzone))
  and info = Cmd.info "ozone" ~version:"10.2.2"
  in
  Cmd.v info term

let () = exit (Cmd.eval cmd)
