open Lwt.Infix

let ( <.> ) f g x = f (g x)
let apply v f = f v

let reporter ppf =
  let report src level ~over k msgf =
    let k _ =
      over () ;
      k () in
    let with_metadata header _tags k ppf fmt =
      Format.kfprintf k ppf
        ("%a[%a]: " ^^ fmt ^^ "\n%!")
        Logs_fmt.pp_header (level, header)
        Fmt.(styled `Magenta string)
        (Logs.Src.name src) in
    msgf @@ fun ?header ?tags fmt -> with_metadata header tags k ppf fmt in
  { Logs.report }

let () = Fmt_tty.setup_std_outputs ~style_renderer:`Ansi_tty ~utf_8:true ()
let () = Logs.set_reporter (reporter Fmt.stderr)
let () = Logs.set_level ~all:true (Some Logs.Debug)
let () = Mirage_crypto_rng_unix.use_default ()

module P = Paf_mirage.Make (Tcpip_stack_socket.V4V6.TCP)

let unix_stack () =
  Tcpip_stack_socket.V4V6.UDP.connect ~ipv4_only:false ~ipv6_only:false
    Ipaddr.V4.Prefix.global None
  >>= fun udpv4 ->
  Tcpip_stack_socket.V4V6.TCP.connect ~ipv4_only:false ~ipv6_only:false
    Ipaddr.V4.Prefix.global None
  >>= fun tcpv4 -> Tcpip_stack_socket.V4V6.connect udpv4 tcpv4

let error_handler (_ip, _port) ?request:_ _error _respond = ()

let load_file filename =
  let ic = open_in filename in
  let ln = in_channel_length ic in
  let rs = Bytes.create ln in
  really_input ic rs 0 ln ;
  close_in ic ;
  Bytes.unsafe_to_string rs

let tls =
  let cert = load_file "server.pem" in
  let key = load_file "server.key" in
  match
    (X509.Certificate.decode_pem_multiple cert, X509.Private_key.decode_pem key)
  with
  | Ok certs, Ok (`RSA key) ->
      Result.get_ok
        (Tls.Config.server ~certificates:(`Single (certs, `RSA key)) ())
  | _ -> invalid_arg "Invalid certificate or key"

let sleep = Lwt_unix.sleep <.> Int64.to_float

let run_http_and_https_server ~request_handler stop =
  unix_stack () >|= Tcpip_stack_socket.V4V6.tcp >>= fun stack ->
  P.init ~port:9090 stack >>= fun socket0 ->
  P.init ~port:3434 stack >>= fun socket1 ->
  let http = P.http_service ~error_handler (fun _flow -> request_handler) in
  let https =
    P.https_service ~tls ~error_handler (fun _flow -> request_handler) in
  let (`Initialized fiber0) = P.serve ~stop http socket0 in
  let (`Initialized fiber1) = P.serve ~stop https socket1 in
  Logs.debug (fun m -> m "Server initialised.") ;
  Lwt.async (fun () -> Lwt.join [ fiber0; fiber1 ]) ;
  Lwt.return_unit

let resolver domain_name =
  match Unix.gethostbyname (Domain_name.to_string domain_name) with
  | { Unix.h_addr_list; _ } ->
      if Array.length h_addr_list > 0
      then Lwt.return_some (Ipaddr_unix.of_inet_addr h_addr_list.(0))
      else Lwt.return_none
  | exception _ -> Lwt.return_none

let tcp_connect scheme stack ipaddr port =
  match scheme with
  | `HTTP -> Lwt.return_some (stack, ipaddr, port)
  | _ -> Lwt.return_none

let tls_connect scheme domain_name cfg stack ipaddr port =
  match scheme with
  | `HTTPS -> Lwt.return_some (domain_name, cfg, stack, ipaddr, port)
  | _ -> Lwt.return_none

let null =
  let authenticator ?ip:_ ~host:_ _ = Ok None in
  Result.get_ok (Tls.Config.client ~authenticator ())

module Client = Paf_cohttp

let stack = Mimic.make ~name:"stack"

let ctx =
  let tls = Mimic.make ~name:"tls" in
  Mimic.empty
  |> Mimic.(
       fold P.tcp_edn
         Fun.
           [
             req Paf_cohttp.scheme;
             req stack;
             req Paf_cohttp.ipaddr;
             dft Paf_cohttp.port 9090;
           ]
         ~k:tcp_connect)
  |> Mimic.(
       fold P.tls_edn
         Fun.
           [
             req Paf_cohttp.scheme;
             opt Paf_cohttp.domain_name;
             dft tls null;
             req stack;
             req Paf_cohttp.ipaddr;
             dft Paf_cohttp.port 3434;
           ]
         ~k:tls_connect)
  |> Mimic.(
       fold Paf_cohttp.ipaddr Fun.[ req Paf_cohttp.domain_name ] ~k:resolver)

let body_to_string body =
  let buf = Buffer.create 0x100 in
  let th, wk = Lwt.wait () in
  let on_eof () =
    Lwt.wakeup_later wk (Buffer.contents buf) ;
    H1.Body.Reader.close body in
  let rec on_read str ~off ~len =
    let str = Bigstringaf.substring str ~off ~len in
    Logs.debug (fun m -> m "Received %S." str) ;
    Buffer.add_string buf str ;
    H1.Body.Reader.schedule_read body ~on_eof ~on_read in
  Logs.debug (fun m -> m "Start to receive the body.") ;
  H1.Body.Reader.schedule_read body ~on_eof ~on_read ;
  th

let query_to_assoc str =
  let lst =
    Astring.String.fields ~is_sep:(function '&' -> true | _ -> false) str in
  let f str =
    match Astring.String.cut ~sep:"=" str with
    | Some (k, v) -> (k, v)
    | None -> (str, "") in
  List.map f lst

let request_handler (ip, port) reqd =
  let open H1 in
  let req = Reqd.request reqd in
  Logs.debug (fun m ->
      m "Got a connection from %a:%d %s." Ipaddr.pp ip port req.Request.target) ;
  let body = Reqd.request_body reqd in
  match req.Request.target with
  | "/" ->
      let contents = "Hello World!" in
      let headers =
        Headers.of_list
          [ ("content-length", string_of_int (String.length contents)) ] in
      let resp = Response.create ~headers `OK in
      Reqd.respond_with_string reqd resp contents ;
      Lwt.async @@ fun () ->
      body_to_string body >|= fun _ -> Logs.debug (fun m -> m "Body drained.")
  | "/repeat" ->
      Lwt.async @@ fun () ->
      body_to_string body >|= fun str ->
      let headers =
        Headers.of_list
          [ ("content-length", string_of_int (String.length str)) ] in
      let resp = Response.create ~headers `OK in
      Reqd.respond_with_string reqd resp str
  | target ->
  match Astring.String.cut ~sep:"?" target with
  | Some ("/query", query) ->
      let lst = query_to_assoc query in
      let buf = Buffer.create 0x100 in
      let ppf = Format.formatter_of_buffer buf in
      Fmt.pf ppf "%a%!"
        Fmt.(list ~sep:(any ";") (pair ~sep:(any "=") string string))
        lst ;
      let contents = Buffer.contents buf in
      let headers =
        Headers.of_list
          [ ("content-length", string_of_int (String.length contents)) ] in
      let resp = Response.create ~headers `OK in
      Reqd.respond_with_string reqd resp contents ;
      Lwt.async @@ fun () ->
      body_to_string body >>= fun _ -> Lwt.return_unit
  | _ ->
      Reqd.report_exn reqd Not_found ;
      let contents = "Invalid request." in
      let headers =
        Headers.of_list
          [ ("content-length", string_of_int (String.length contents)) ] in
      let resp = Response.create ~headers `Bad_request in
      Reqd.respond_with_string reqd resp contents

let test01 =
  Alcotest_lwt.test_case "simple-http" `Quick @@ fun _sw () ->
  unix_stack () >|= Tcpip_stack_socket.V4V6.tcp >>= fun v ->
  let ctx = Mimic.add stack v ctx in
  Client.get ~ctx (Uri.of_string "http://localhost:9090/")
  >>= fun (_resp, body) ->
  Cohttp_lwt.Body.to_string body >>= fun str ->
  Alcotest.(check string) "contents" str "Hello World!" ;
  Lwt.return_unit

let test02 =
  Alcotest_lwt.test_case "repeat" `Quick @@ fun _sw () ->
  unix_stack () >|= Tcpip_stack_socket.V4V6.tcp >>= fun v ->
  let ctx = Mimic.add stack v ctx in
  let body = Cohttp_lwt.Body.of_string "Hello!" in
  Client.post ~ctx ~body (Uri.of_string "http://localhost:9090/repeat")
  >>= fun (_resp, body) ->
  Cohttp_lwt.Body.to_string body >>= fun str ->
  Alcotest.(check string) "contents" str "Hello!" ;
  Lwt.return_unit

let test03 =
  Alcotest_lwt.test_case "simple-https" `Quick @@ fun _sw () ->
  unix_stack () >|= Tcpip_stack_socket.V4V6.tcp >>= fun v ->
  let ctx = Mimic.add stack v ctx in
  Client.get ~ctx (Uri.of_string "https://localhost:3434/")
  >>= fun (_resp, body) ->
  Cohttp_lwt.Body.to_string body >>= fun str ->
  Alcotest.(check string) "contents" str "Hello World!" ;
  Lwt.return_unit

let test04 =
  Alcotest_lwt.test_case "repeat (https)" `Quick @@ fun _sw () ->
  unix_stack () >|= Tcpip_stack_socket.V4V6.tcp >>= fun v ->
  let ctx = Mimic.add stack v ctx in
  let body = Cohttp_lwt.Body.of_string "Secret Hello!" in
  Client.post ~ctx ~body (Uri.of_string "https://localhost:3434/repeat")
  >>= fun (_resp, body) ->
  Cohttp_lwt.Body.to_string body >>= fun str ->
  Alcotest.(check string) "contents" str "Secret Hello!" ;
  Lwt.return_unit

let test05 =
  Alcotest_lwt.test_case "queries" `Quick @@ fun _sw () ->
  unix_stack () >|= Tcpip_stack_socket.V4V6.tcp >>= fun v ->
  let ctx = Mimic.add stack v ctx in
  Client.get ~ctx (Uri.of_string "https://localhost:3434/query?foo=a&bar=b")
  >>= fun (_resp, body) ->
  Cohttp_lwt.Body.to_string body >>= fun str ->
  Alcotest.(check string) "contents" str "foo=a;bar=b" ;
  Lwt.return_unit

let test () =
  Alcotest_lwt.run "smart"
    [ ("cohttp", [ test01; test02; test03; test04; test05 ]) ]

let () =
  let fiber =
    Lwt_switch.with_switch @@ fun stop ->
    run_http_and_https_server ~request_handler stop >>= test >>= fun () ->
    Lwt_switch.turn_off stop in
  Lwt_main.run fiber
