(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)

open Dns

module Canonical : sig
  type 'a t
  val v : 'a Domain_name.t -> 'a t
  val d : 'a t -> 'a Domain_name.t
end = struct
  type 'a t = 'a Domain_name.t
  let v n = Domain_name.canonical n
  let d n = n
end

module O = struct
  type t = string
  let compare = String.compare
end
module M = Map.Make(O)

type t = N of t M.t * Rr_map.t

let empty = N (M.empty, Rr_map.empty)

let is_empty (N (sub, map)) = M.is_empty sub && Rr_map.is_empty map

let bindings t =
  let rec go pre (N (sub, e)) =
    let subs = M.bindings sub in
    (pre, e) ::
    List.fold_left
      (fun acc (pre', va) ->
         acc @ go (Domain_name.prepend_label_exn pre pre') va) [] subs
  in
  go Domain_name.root t

let pp_map name ppf map =
  Fmt.(list ~sep:(any "@.") string) ppf
    (List.map (Rr_map.text_b name) (Rr_map.bindings map))

let pp ppf t = List.iter (fun (name, map) -> pp_map name ppf map) (bindings t)

let rec equal (N (sub, map)) (N (sub', map')) =
  Rr_map.equal { f = Rr_map.equal_rr } map map' && M.equal equal sub sub'

type e = [ `Delegation of [ `raw ] Domain_name.t * (int32 * Domain_name.Host_set.t)
         | `EmptyNonTerminal of [ `raw ] Domain_name.t * Soa.t
         | `NotAuthoritative
         | `NotFound of [ `raw ] Domain_name.t * Soa.t ]

let pp_e ppf = function
  | `Delegation (name, (ttl, ns)) ->
    Fmt.pf ppf "delegation %a to TTL %lu %a" Domain_name.pp name ttl
      Fmt.(list ~sep:(any ",@,") Domain_name.pp) (Domain_name.Host_set.elements ns)
  | `EmptyNonTerminal (name, soa) ->
    Fmt.pf ppf "empty non terminal %a SOA %a" Domain_name.pp name Soa.pp soa
  | `NotAuthoritative -> Fmt.string ppf "not authoritative"
  | `NotFound (name, soa) -> Fmt.pf ppf "not found %a soa %a" Domain_name.pp name Soa.pp soa


let ( let* ) = Result.bind

let guard p err = if p then Ok () else Error err

let ent name map =
  let soa = Rr_map.get Soa map in
  `EmptyNonTerminal (name, soa)

let to_ns name map =
  let ttl, ns =
    match Rr_map.find Ns map with
    | None -> 0l, Domain_name.Host_set.empty
    | Some (ttl, ns) -> ttl, ns
  in
  (name, ttl, ns)

let check_zone = function
  | None -> Error `NotAuthoritative
  | Some (`Delegation (name, (ttl, ns))) -> Error (`Delegation (name, (ttl, ns)))
  | Some (`Soa (z, zmap)) -> Ok (z, zmap)

let lookup_res zone ty m =
  let* z, zmap = check_zone zone in
  match Rr_map.find ty m with
  | Some v -> Ok (Rr_map.B (ty, v), to_ns z zmap)
  | None -> match Rr_map.find Cname m with
    | None when Rr_map.cardinal m = 1 && Rr_map.(mem Soa m) ->
      (* this is primary a hack for localhost, which must be NXDomain,
         but there's a SOA for localhost (to handle it authoritatively) *)
      (* TODO should we check that the label-node map is empty?
         well, if we have a proper authoritative zone, there'll be a NS *)
      let soa = Rr_map.get Rr_map.Soa zmap in
      Error (`NotFound (z, soa))
    | None -> Error (ent z zmap)
    | Some cname -> Ok (B (Cname, cname), to_ns z zmap)

let lookup_aux name t =
  let name = Canonical.d name in
  let k = Domain_name.to_array name in
  let l = Array.length k in
  let fzone idx map =
    let name = Domain_name.(of_array (Array.sub (to_array name) 0 idx)) in
    match Rr_map.mem Soa map, Rr_map.find Ns map with
    | true, _ -> Some (`Soa (name, map))
    | false, Some ns -> Some (`Delegation (name, ns))
    | false, None -> None
  in
  let rec go idx zone = function
    | N (sub, map) ->
      let zone = match fzone idx map with None -> zone | Some x -> Some x in
      if idx = l then Ok (zone, sub, map)
      else match M.find (Array.get k idx) sub with
        | exception Not_found ->
          begin match zone with
            | None -> Error `NotAuthoritative
            | Some (`Delegation (name, (ttl, ns))) ->
              Error (`Delegation (name, (ttl, ns)))
            | Some (`Soa (name, map)) ->
              (* may still be a wildcard *)
              match M.find "*" sub with
              | exception Not_found ->
                let soa = Rr_map.get Soa map in
                Error (`NotFound (name, soa))
              | N (sub, map) -> Ok (zone, sub, map)
          end
        | x -> go (succ idx) zone x
  in
  go 0 None t

let lookup_with_cname name ty t =
  let* zone, _sub, map = lookup_aux (Canonical.v name) t in
  lookup_res zone ty map

let lookup name key t =
  let* zone, _sub, map = lookup_aux (Canonical.v name) t in
  let* z, zmap = check_zone zone in
  Option.to_result ~none:(ent z zmap) (Rr_map.find key map)

let lookup_any name t =
  match lookup_aux (Canonical.v name) t with
  | Error e -> Error e
  | Ok (zone, _sub, m) ->
    let* z, zmap = check_zone zone in
    Ok (m, to_ns z zmap)

let lookup_glue name t =
  match lookup_aux (Canonical.v name) t with
  | Error _ -> None, None
  | Ok (_zone, _sub, map) -> Rr_map.find A map, Rr_map.find Aaaa map

let zone name t =
  match lookup_aux (Canonical.v name) t with
  | Error (`NotFound (zone, soa)) -> Ok (zone, soa)
  | Error e -> Error e
  | Ok (zone, _, _) ->
    match check_zone zone with
    | Error e -> Error e
    | Ok (name, map) ->
      (* we ended with `Soa, which checked that map contains a Soa *)
      Ok (name, Rr_map.get Soa map)

let fold key (N (sub, map)) f s =
  let get name map acc =
    match Rr_map.find key map with
    | Some a -> f name a acc
    | None -> acc
  in
  let rec collect name sub acc =
    List.fold_left (fun acc (pre, N (sub, map)) ->
        let n' = Domain_name.prepend_label_exn name pre in
        let keys = get n' map acc in
        collect n' sub keys)
      acc (M.bindings sub)
  in
  let name = Domain_name.root in
  collect name sub (get name map s)

let collect_rrs name sub map =
  let collect_map top name rrmap =
    if not top && Rr_map.mem Ns rrmap then
      (* delegation *)
      let ns_entries =
        Option.fold ~none:[]
          ~some:(fun ns -> [ name, Rr_map.B (Ns, ns) ])
          (Rr_map.find Ns rrmap)
      and ds_entries =
        Option.fold ~none:[]
          ~some:(fun ds -> [ name, Rr_map.B (Ds, ds) ])
          (Rr_map.find Ds rrmap)
      and rrsig_entries =
        Option.fold ~none:[]
          ~some:(fun rrsig -> [ name, Rr_map.B (Rrsig, rrsig) ])
          (Rr_map.find Rrsig rrmap)
      in
      ns_entries @ ds_entries @ rrsig_entries, false
    else
      Rr_map.fold (fun v acc -> (name, v) :: acc) rrmap [], true
  in
  let rec go top name sub map =
    let entries, recurse = collect_map top name map in
    if recurse then
      List.fold_left
        (fun acc (pre, N (sub, map)) ->
           acc @ go false (Domain_name.prepend_label_exn name pre) sub map)
        entries (M.bindings sub)
    else
      entries
  in
  go true name sub map

let collect_entries name sub map =
  let ttlsoa =
    match Rr_map.find Soa map with
    | Some v -> Some v
    | None when Domain_name.count_labels name = 0 ->
      Some { Soa.nameserver = Domain_name.root ;
             hostmaster = Domain_name.root ;
             serial = 0l ; refresh = 0l ; retry = 0l ;
             expiry = 0l ; minimum = 0l }
    | None -> None
  in
  match ttlsoa with
  | None -> Error `NotAuthoritative
  | Some soa ->
    let entries = collect_rrs name sub (Rr_map.remove Soa map) in
    let res =
      List.fold_left (fun acc (name, (Rr_map.B (k, v))) ->
          Name_rr_map.add name k v acc) Domain_name.Map.empty entries
    in
    Ok (soa, res)

let entries name t =
  let name = Domain_name.raw name in
  let* zone, sub, map = lookup_aux (Canonical.v name) t in
  match zone with
  | None -> Error `NotAuthoritative
  | Some (`Delegation (name, (ttl, ns))) ->
    Error (`Delegation (name, (ttl, ns)))
  | Some (`Soa (name', _)) when Domain_name.equal name name' ->
    collect_entries name sub map
  | Some (`Soa (_, _)) -> Error `NotAuthoritative

type zone_check = [ `Missing_soa of [ `raw ] Domain_name.t
                  | `Cname_other of [ `raw ] Domain_name.t
                  | `Bad_ttl of [ `raw ] Domain_name.t * Rr_map.b
                  | `Empty of [ `raw ] Domain_name.t * Rr_map.k
                  | `Missing_address of [ `host ] Domain_name.t
                  | `Soa_not_a_host of [ `raw ] Domain_name.t * string ]

let pp_zone_check ppf = function
  | `Missing_soa name -> Fmt.pf ppf "missing soa for %a" Domain_name.pp name
  | `Cname_other name -> Fmt.pf ppf "%a contains a cname record, and also other entries" Domain_name.pp name
  | `Bad_ttl (name, v) -> Fmt.pf ppf "bad TTL for %a %a" Domain_name.pp name Rr_map.pp_b v
  | `Empty (name, typ) -> Fmt.pf ppf "%a empty %a" Domain_name.pp name Rr_map.ppk typ
  | `Missing_address name -> Fmt.pf ppf "missing address record for %a" Domain_name.pp name
  | `Soa_not_a_host (name, msg) -> Fmt.pf ppf "%a the SOA nameserver is not a hostname: %s" Domain_name.pp name msg

(* TODO: check for no cname loops? and dangling cname!? *)
let check trie =
  let has_address name =
    match lookup name Rr_map.A trie with
    | Ok _ -> true
    | Error (`Delegation _) -> true
    | _ -> match lookup name Rr_map.Aaaa trie with
      | Ok _ -> true
      | _ -> false
  in
  let rec check_sub names state sub map =
    let name = Domain_name.of_strings_exn names in
    let state' =
      match Rr_map.find Soa map with
      | None -> begin match Rr_map.find Ns map with
          | None -> state
          | Some _ -> `None
        end
      | Some _ -> `Soa name
    in
    let* () =
      guard ((Rr_map.mem Cname map && Rr_map.cardinal map = 1) ||
             not (Rr_map.mem Cname map)) (`Cname_other name)
    in
    let* () =
      Rr_map.fold (fun v r ->
          let* () = r in
          match v with
          | B (Dnskey, (ttl, keys)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Rr_map.Dnskey_set.is_empty keys then
              Error (`Empty (name, Rr_map.K Dnskey))
            else Ok ()
          | B (Ns, (ttl, names)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Domain_name.Host_set.is_empty names then
              Error (`Empty (name, K Ns))
            else
              let domain = match state' with `None -> name | `Soa zone -> zone in
              Domain_name.Host_set.fold (fun name r ->
                  let* () = r in
                  if Domain_name.is_subdomain ~subdomain:name ~domain then
                    guard (has_address name) (`Missing_address name)
                  else
                    Ok ()) names (Ok ())
          | B (Cname, (ttl, _)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v)) else Ok ()
          | B (Mx, (ttl, mxs)) ->
            if ttl < 0l then
              Error (`Bad_ttl (name, v))
            else if Rr_map.Mx_set.is_empty mxs then
              Error (`Empty (name, K Mx))
            else
              let domain = match state' with `None -> name | `Soa zone -> zone in
              Rr_map.Mx_set.fold (fun { mail_exchange ; _ } r ->
                  let* () = r in
                  if Domain_name.is_subdomain ~subdomain:mail_exchange ~domain then
                    guard (has_address mail_exchange) (`Missing_address mail_exchange)
                  else
                    Ok ())
                mxs (Ok ())
          | B (Ptr, (ttl, name)) ->
            if ttl < 0l then Error (`Bad_ttl (Domain_name.raw name, v)) else Ok ()
          | B (Soa, soa) ->
            begin match Domain_name.host soa.nameserver with
              | Error (`Msg m) -> Error (`Soa_not_a_host (soa.nameserver, m))
              | Ok _ -> Ok ()
            end
          | B (Txt, (ttl, txts)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Rr_map.Txt_set.is_empty txts then
              Error (`Empty (name, K Txt))
            else if
              Rr_map.Txt_set.exists (fun s -> String.length s > 0) txts
            then
              Ok ()
            else
              Error (`Empty (name, K Txt))
          | B (A, (ttl, a)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Ipaddr.V4.Set.is_empty a then
              Error (`Empty (name, K A))
            else Ok ()
          | B (Aaaa, (ttl, aaaa)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Ipaddr.V6.Set.is_empty aaaa then
              Error (`Empty (name, K Aaaa))
            else Ok ()
          | B (Srv, (ttl, srvs)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Rr_map.Srv_set.is_empty srvs then
              Error (`Empty (name, K Srv))
            else Ok ()
          | B (Svcb, (ttl, svcbs)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Rr_map.Svcb_set.is_empty svcbs then
              Error (`Empty (name, K Svcb))
            else Ok ()
          | B (Https, (ttl, httpss)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Rr_map.Https_set.is_empty httpss then
              Error (`Empty (name, K Https))
            else Ok ()
          | B (Caa, (ttl, caas)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Rr_map.Caa_set.is_empty caas then
              Error (`Empty (name, K Caa))
            else Ok ()
          | B (Tlsa, (ttl, tlsas)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Rr_map.Tlsa_set.is_empty tlsas then
              Error (`Empty (name, K Tlsa))
            else Ok ()
          | B (Sshfp, (ttl, sshfps)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Rr_map.Sshfp_set.is_empty sshfps then
              Error (`Empty (name, K Sshfp))
            else Ok ()
          | B (Ds, (ttl, ds)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Rr_map.Ds_set.is_empty ds then
              Error (`Empty (name, K Ds))
            else Ok ()
          | B (Rrsig, (ttl, rrs)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Rr_map.Rrsig_set.is_empty rrs then
              Error (`Empty (name, K Rrsig))
            else Ok ()
          | B (Nsec, (ttl, _rr)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else Ok ()
          | B (Nsec3, (ttl, _rr)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else Ok ()
          | B (Loc, (ttl, locs)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Rr_map.Loc_set.is_empty locs then
              Error (`Empty (name, Rr_map.K Loc))
            else Ok ()
          | B (Null, (ttl, nulls)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Rr_map.Null_set.is_empty nulls then
              Error (`Empty (name, Rr_map.K Null))
            else Ok ()
          | B (Unknown x, (ttl, datas)) ->
            if ttl < 0l then Error (`Bad_ttl (name, v))
            else if Rr_map.Txt_set.is_empty datas then
              Error (`Empty (name, K (Unknown x)))
            else Ok ())
        map (Ok ())
    in
    M.fold (fun lbl (N (sub, map)) r ->
        let* () = r in
        check_sub (lbl :: names) state' sub map) sub (Ok ())
  in
  let (N (sub, map)) = trie in
  check_sub [] `None sub map

let find f name t =
  let name = Canonical.d name in
  let lbls = Domain_name.to_array name in
  let l = Array.length lbls in
  let rec go idx (N (sub, map)) =
    if idx = l then
      let sub', map' = f sub map in
      N (sub', map')
    else
      let lbl = Array.get lbls idx in
      let node = match M.find lbl sub with
        | exception Not_found -> empty
        | x -> x
      in
      let node' = go (succ idx) node in
      if is_empty node' then
        N (M.remove lbl sub, map)
      else
        N (M.add lbl node' sub, map)
  in
  go 0 t

let replace name k v t =
  find (fun sub map -> sub, Rr_map.add k v map) (Canonical.v name) t

let insert name k v t =
  let name = Canonical.v name in
  let merge sub map =
    let new_v = match Rr_map.find k map with
      | None -> v
      | Some v' -> Rr_map.union_rr k v' v
    in
    sub, Rr_map.add k new_v map
  in
  find merge name t

let replace_map m t =
  Domain_name.Map.fold (fun name map trie ->
      find (fun sub _ -> sub, map) (Canonical.v name) trie) m t

let insert_map m t =
  Domain_name.Map.fold (fun name map trie ->
      let union sub old = sub, Rr_map.union { f = Rr_map.unionee } old map in
      find union (Canonical.v name) trie)
    m t

let remove k ty v t =
  let remove sub map =
    let map' = match Rr_map.find ty map with
      | None -> map
      | Some old -> match Rr_map.remove_rr ty old v with
        | None -> Rr_map.remove ty map
        | Some v' -> Rr_map.add ty v' map
    in
    sub, map'
  in
  find remove (Canonical.v k) t

let remove_ty k ty t =
  let remove sub map = sub, Rr_map.remove ty map in
  find remove (Canonical.v k) t

let remove_all k t =
  let remove sub _ = sub, Rr_map.empty in
  find remove (Canonical.v k) t

let remove_map m t =
  let merge k present remove = match present, remove with
    | None, None -> None
    | Some x, None -> Some x
    | None, Some _ -> None
    | Some x, Some y -> Rr_map.remove_rr k x y
  in
  Domain_name.Map.fold (fun name map trie ->
      let remove sub old = sub, Rr_map.merge { f = merge } old map in
      find remove (Canonical.v name) trie)
    m t

let remove_zone name t =
  let remove sub _ =
    (* go through all of sub, and retain those subtrees with Soa *)
    let rec fold_sub sub =
      M.fold (fun lbl node acc ->
          match go node with None -> acc | Some n -> M.add lbl n acc)
        sub M.empty
    and go (N (sub, map)) =
      match Rr_map.find Soa map with
      | None ->
        (* no SOA, continue search *)
        let sub' = fold_sub sub in
        if M.is_empty sub' then None else Some (N (sub', Rr_map.empty))
      | Some _ ->
        (* SOA, retain this submap *)
        Some (N (sub, map))
    in
    fold_sub sub, Rr_map.empty (* drop the initial RRmap in any case! *)
  in
  find remove (Canonical.v name) t

let diff zone req_soa ~old current =
  match entries zone current with
  | Error _ -> Error (`Msg "couldn't find zone in current trie")
  | Ok (soa, map) ->
    if not (Soa.newer ~old:req_soa soa) then
      Ok (soa, `Empty)
    else
      match entries zone old with
      | Error _ -> Ok (soa, `Full map)
      | Ok (oldsoa, oldmap) ->
        (* first, we fold over old map and collect the differences in two maps *)
        let (to_remove, to_add), names =
          Domain_name.Map.fold (fun name old ((to_remove, to_add), names) ->
              let newmap =
                match Domain_name.Map.find name map with
                | None -> Rr_map.empty | Some x -> x
              in
              (match Rr_map.diff ~old newmap with
               | None, None -> to_remove, to_add
               | Some rm, None -> Domain_name.Map.add name rm to_remove, to_add
               | None, Some add -> to_remove, Domain_name.Map.add name add to_add
               | Some rm, Some add ->
                 Domain_name.Map.add name rm to_remove,
                 Domain_name.Map.add name add to_add),
              Domain_name.Set.add name names)
            oldmap Domain_name.((Map.empty, Map.empty), Set.empty)
        in
        (* now we fold over newmap and add then unless already handled *)
        let to_add =
          Domain_name.Map.fold (fun name newmap to_add ->
              if Domain_name.Set.mem name names then
                to_add
              else
                Domain_name.Map.add name newmap to_add)
            map to_add
        in
        Ok (soa, `Difference (oldsoa, to_remove, to_add))
