let ( let* ) = Result.bind

module Ext = struct

  type _ k =
    | Password : string k
    | Name : string k
    | Extensions : Extension.t k

  module K = struct
    type 'a t = 'a k

    let compare : type a b . a t -> b t -> (a, b) Gmap.Order.t = fun t t' ->
      let open Gmap.Order in
      match t, t' with
      | Password, Password -> Eq | Password, _ -> Lt | _, Password -> Gt
      | Name, Name -> Eq | Name, _ -> Lt | _, Name -> Gt
      | Extensions, Extensions -> Eq
  end

  include Gmap.Make(K)

  let pp_one : type a. a k -> Format.formatter -> a -> unit = fun k ppf v ->
    match k, v with
    | Password, pass -> Fmt.pf ppf "password %s" pass
    | Name, name -> Fmt.pf ppf "name %s" name
    | Extensions, ext -> Fmt.pf ppf "extensions %a" Extension.pp ext

  let pp ppf m = iter (fun (B (k, v)) -> pp_one k ppf v ; Fmt.sp ppf ()) m
end

type request_info = {
  subject : Distinguished_name.t ;
  public_key : Public_key.t ;
  extensions : Ext.t ;
}

type request = {
  info : request_info ;
  signature_algorithm : Algorithm.t ;
  signature : string
}

type t = {
  asn : request ;
  raw : string ;
}

module Asn = struct
  open Asn_grammars
  open Asn.S
  open Registry

  let attributes =
    let f = function[@ocaml.warning "-8"]
      | (oid, [`C1 p]) when oid = PKCS9.challenge_password -> Ext.B (Password, p)
      | (oid, [`C1 n]) when oid = PKCS9.unstructured_name -> Ext.B (Name, n)
      | (oid, [`C2 es]) when oid = PKCS9.extension_request -> Ext.B (Extensions, es)
    and g (Ext.B (k, v)) : Asn.oid * [ `C1 of string | `C2 of Extension.t ] list = match k, v with
      | Ext.Password, v -> (PKCS9.challenge_password, [`C1 v])
      | Ext.Name, v -> (PKCS9.unstructured_name, [`C1 v])
      | Ext.Extensions, v -> (PKCS9.extension_request, [`C2 v])
    in
    map f g @@
    sequence2
      (required ~label:"attr type" oid)
      (required ~label:"attr value"
         (set_of (choice2
                    utf8_string
                    Extension.Asn.extensions_der)))
  let request_info =
    let f = function
      | (0, subject, public_key, extensions) ->
        let extensions =
          List.fold_left (fun map (Ext.B (k, v)) ->
              match Ext.add_unless_bound k v map with
              | None -> parse_error "request extension %a already bound"
                          (Ext.pp_one k) v
              | Some b -> b)
        Ext.empty extensions
        in
        { subject ; public_key ; extensions }
      | _ ->
        parse_error "unknown certificate request info"
    and g { subject ; public_key ; extensions } =
      let extensions = Ext.bindings extensions in
      (0, subject, public_key, extensions)
    in
    map f g @@
    sequence4
      (required ~label:"version" int)
      (required ~label:"subject" Distinguished_name.Asn.name)
      (required ~label:"subjectPKInfo" Public_key.Asn.pk_info_der)
      (required ~label:"attributes" @@ implicit 0 (set_of attributes))

  let request_info_of_str, request_info_to_str =
    projections_of Asn.der request_info

  let signing_request =
    let f = fun (info, signature_algorithm, signature) ->
      { info ; signature_algorithm ; signature }
    and g = fun { info ; signature_algorithm ; signature } ->
      (info, signature_algorithm, signature)
    in
    map f g @@
    sequence3
      (required ~label:"certificationRequestInfo" request_info)
      (required ~label:"signatureAlgorithm" Algorithm.identifier)
      (required ~label:"signature" bit_string_octets)

  let signing_request_of_str, signing_request_to_str =
    projections_of Asn.der signing_request
end

let info { asn ; _ } = asn.info

let signature_algorithm { asn ; _ } =
  Algorithm.to_signature_algorithm asn.signature_algorithm

let hostnames csr =
  let info = info csr in
  let subj =
    match Distinguished_name.common_name info.subject with
    | None -> Host.Set.empty
    | Some x ->
      match Host.host x with
      | Some (typ, n) -> Host.Set.singleton (typ, n)
      | None -> Host.Set.empty
  in
  match Ext.(find Extensions info.extensions) with
  | None -> subj
  | Some exts -> match Extension.hostnames exts with
    | Some names -> names
    | None -> subj

let validate_signature allowed_hashes { asn ; raw } =
  let raw_data = Validation.raw_cert_hack raw in
  Validation.validate_raw_signature asn.info.subject allowed_hashes raw_data
    asn.signature_algorithm asn.signature asn.info.public_key

let decode_der ?(allowed_hashes = Validation.sha2) cs =
  let* csr = Asn_grammars.err_to_msg (Asn.signing_request_of_str cs) in
  let csr = { raw = cs ; asn = csr } in
  let* () =
    Result.map_error
      (fun e -> `Msg (Fmt.to_to_string Validation.pp_signature_error e))
      (validate_signature allowed_hashes csr)
  in
  Ok csr

let encode_der { raw ; _ } = raw

let decode_pem cs =
  let* data = Pem.parse cs in
  let crs =
    List.filter (fun (t, _) -> String.equal "CERTIFICATE REQUEST" t) data
  in
  let* csrs = Pem.foldM (fun (_, cs) -> decode_der cs) crs in
  Pem.exactly_one ~what:"certificate request" csrs

let encode_pem v =
  Pem.unparse ~tag:"CERTIFICATE REQUEST" (encode_der v)

let digest_of_key = function
  | `RSA _ -> `SHA256
  | `ED25519 _ -> `SHA512
  | `P256 _ -> `SHA256
  | `P384 _ -> `SHA384
  | `P521 _ -> `SHA512

let default_digest digest key =
  match digest with None -> digest_of_key key | Some x -> x

let create subject ?digest ?(extensions = Ext.empty) (key : Private_key.t) =
  let hash = default_digest digest key in
  let public_key = Private_key.public key in
  let info : request_info = { subject ; public_key ; extensions } in
  let info_str = Asn.request_info_to_str info in
  let scheme = Key_type.x509_default_scheme (Private_key.key_type key) in
  let* signature = Private_key.sign hash ~scheme key (`Message info_str) in
  let signature_algorithm = Algorithm.of_signature_algorithm scheme hash in
  let asn = { info ; signature_algorithm ; signature } in
  let raw = Asn.signing_request_to_str asn in
  Ok { asn ; raw }

let sign signing_request
    ~valid_from ~valid_until
    ?(allowed_hashes = Validation.sha2)
    ?digest
    ?serial
    ?(extensions = Extension.empty)
    ?(subject = signing_request.asn.info.subject)
    key issuer =
  let hash = default_digest digest key in
  let serial = match serial with
    | Some s -> s
    | None ->
      (* we generate a positive integer, asn1-encoded: so if the high bit is
         set, we prepend a 0 byte *)
      (* if it starts with 0x00 followed by 0xNN with NN <= 0x7f, we prepend
         0x7f to make the integer valid *)
      let s = Mirage_crypto_rng.generate 10 in
      let start = String.get_uint8 s 0 in
      if start > 0x7f then
        "\x00" ^ s
      else if start = 0x00 && String.get_uint8 s 1 <= 0x7f then
        "\x7f" ^ s
      else
        s
  in
  let* () = validate_signature allowed_hashes signing_request in
  let signature_algo =
    let scheme = Key_type.x509_default_scheme (Private_key.key_type key) in
    Algorithm.of_signature_algorithm scheme hash
  and info = signing_request.asn.info
  in
  let tbs_cert : Certificate.tBSCertificate = {
    version = `V3 ;
    serial ;
    signature = signature_algo ;
    issuer = issuer ;
    validity = (valid_from, valid_until) ;
    subject ;
    pk_info = info.public_key ;
    issuer_id = None ;
    subject_id = None ;
    extensions
  } in
  let tbs_raw = Certificate.Asn.tbs_certificate_to_octets tbs_cert in
  let scheme = Key_type.x509_default_scheme (Private_key.key_type key) in
  let* signature_val = Private_key.sign hash ~scheme key (`Message tbs_raw) in
  let asn = {
    Certificate.tbs_cert ;
    signature_algo ;
    signature_val ;
  } in
  let raw = Certificate.Asn.certificate_to_octets asn in
  Ok { Certificate.asn ; raw }
