(*----------------------------------------------------------------------------
    Copyright (c) 2017 Inhabited Type LLC.
    Copyright (c) 2025 Robur Cooperative

    All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions
    are met:

    1. Redistributions of source code must retain the above copyright
       notice, this list of conditions and the following disclaimer.

    2. Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions and the following disclaimer in the
       documentation and/or other materials provided with the distribution.

    3. Neither the name of the author nor the names of his contributors
       may be used to endorse or promote products derived from this software
       without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS
    OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
    ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
    OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
    HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
    STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.
  ----------------------------------------------------------------------------*)

open Httpun_types

module Queue = struct
  include Queue

  let peek_exn = peek
  let peek t = if is_empty t then None else Some (peek_exn t)
end

module Reader = Parse.Reader
module Writer = Serialize.Writer

type request_handler = Reqd.t -> unit

type error =
  [ `Bad_gateway | `Bad_request | `Internal_server_error | `Exn of exn ]

type error_handler =
  ?request:Request.t -> error -> (Headers.t -> Body.Writer.t) -> unit

type t = {
  reader : Reader.request;
  writer : Writer.t;
  response_body_buffer : Bstr.t;
  request_handler : request_handler;
  error_handler : error_handler;
  request_queue : Reqd.t Queue.t;
      (* invariant: If [request_queue] is not empty, then the head of the queue
         has already had [request_handler] called on it. *)
  mutable is_errored : bool;
      (* if there is a parse or connection error, we invoke the [error_handler]
         and set [is_errored] to indicate we should not close the writer yet. *)
  mutable wakeup_reader : Optional_thunk.t;
}

let is_closed t = Reader.is_closed t.reader && Writer.is_closed t.writer
let is_active t = not (Queue.is_empty t.request_queue)
let current_reqd_exn t = Queue.peek_exn t.request_queue

let yield_reader t k =
  if is_closed t then failwith "yield_reader on closed conn"
  else if Optional_thunk.is_some t.wakeup_reader then
    failwith "yield_reader: only one callback can be registered at a time"
  else t.wakeup_reader <- Optional_thunk.some k

let wakeup_reader t =
  let f = t.wakeup_reader in
  t.wakeup_reader <- Optional_thunk.none;
  Optional_thunk.call_if_some f

let yield_writer t k =
  if Writer.is_closed t.writer then k () else Writer.on_wakeup t.writer k

let wakeup_writer t = Writer.wakeup t.writer

let default_error_handler ?request:_ error handle =
  let message =
    match error with
    | `Exn exn -> Printexc.to_string exn
    | (#Status.client_error | #Status.server_error) as error ->
        Status.to_string error
  in
  let body = handle Headers.empty in
  Body.Writer.write_string body message;
  Body.Writer.close body

let create ?(config = Config.default) ?(error_handler = default_error_handler)
    request_handler =
  let { Config.response_buffer_size; response_body_buffer_size; _ } = config in
  let writer = Writer.create ~buffer_size:response_buffer_size () in
  let request_queue = Queue.create () in
  let response_body_buffer = Bstr.create response_body_buffer_size in
  let handler request request_body =
    let reqd =
      Reqd.create error_handler request request_body writer response_body_buffer
    in
    Queue.push reqd request_queue
  in
  {
    reader = Reader.request handler;
    writer;
    response_body_buffer;
    request_handler;
    error_handler;
    request_queue;
    is_errored = false;
    wakeup_reader = Optional_thunk.none;
  }

let shutdown_reader t =
  if is_active t then Reqd.close_request_body (current_reqd_exn t);
  Reader.force_close t.reader;
  wakeup_reader t

let shutdown_writer t =
  if is_active t then (
    let reqd = current_reqd_exn t in
    (* XXX(dpatti): I'm not sure I understand why we close the *request* body
       here. Maybe we can write a test such that removing this line causes it to
       fail? *)
    Reqd.close_request_body reqd;
    Reqd.flush_response_body reqd);
  Writer.close t.writer;
  wakeup_writer t

let error_code t =
  if is_active t then Reqd.error_code (current_reqd_exn t) else None

let shutdown t =
  shutdown_reader t;
  shutdown_writer t

let set_error_and_handle ?request t error =
  if is_active t then (
    assert (request = None);
    let reqd = current_reqd_exn t in
    Reqd.report_error reqd error)
  else (
    t.is_errored <- true;
    let status =
      match (error :> [ error | Status.standard ]) with
      | `Exn _ -> `Internal_server_error
      | #Status.standard as status -> status
    in
    shutdown_reader t;
    let writer = t.writer in
    t.error_handler ?request error (fun headers ->
        let response = Response.create ~headers status in
        Writer.write_response writer response;
        let encoding =
          (* If we haven't parsed the request method, just use GET as a standard
             placeholder. The method is only used for edge cases, like HEAD or
             CONNECT. *)
          let request_method =
            match request with None -> `GET | Some request -> request.meth
          in
          match Response.body_length ~request_method response with
          | (`Fixed _ | `Close_delimited) as encoding -> encoding
          | `Chunked ->
              (* XXX(dpatti): Because we pass the writer's faraday directly to the
                 new body, we don't write the chunked encoding. A client won't be
                 able to interpret this. *)
              `Close_delimited
          | `Error (`Bad_gateway | `Internal_server_error) ->
              failwith
                "H1.Server_connection.error_handler: invalid response body \
                 length"
        in
        Body.Writer.of_faraday (Writer.faraday writer) writer ~encoding
    )
  )

let report_exn t exn = set_error_and_handle t (`Exn exn)

let advance_request_queue t =
  ignore (Queue.take t.request_queue);
  if not (Queue.is_empty t.request_queue) then
    t.request_handler (Queue.peek_exn t.request_queue)

let rec _next_read_operation t =
  if not (is_active t)
  then (
    (* If the request queue is empty, there is no connection error, and the
       reader is closed, then we can assume that no more user code will be able
       to write. *)
    if Reader.is_closed t.reader && not t.is_errored then shutdown_writer t;
    Reader.next t.reader)
  else
    let reqd = current_reqd_exn t in
    match Reqd.input_state reqd with
    | Waiting  -> _yield_reader t
    | Ready    -> Reader.next t.reader
    | Complete -> _final_read_operation_for t reqd
    | Upgraded -> `Upgrade

and _final_read_operation_for t reqd =
  if not (Reqd.persistent_connection reqd) then (
    shutdown_reader t;
    Reader.next t.reader;
  ) else (
    match Reqd.output_state reqd with
    | Waiting | Ready -> _yield_reader t
    | Upgraded ->
      (* If the input state is not [Upgraded], the output state cannot be
         either. *)
      assert false
    | Complete ->
      advance_request_queue t;
      _next_read_operation t;
  )

and _yield_reader t =
  (* XXX(dpatti): This is a way in which the reader and writer are not
     parallel -- we tell the writer when it needs to yield but the reader is
     always asking for more data. This is the only branch in either
     operation function that does not return `(Reader|Writer).next`, which
     means there are surprising states you can get into. For example, we ask
     the runtime to yield but then raise when it tries to because the reader
     is closed. I think this can be avoided if we allow this module to tell the
     reader when it should yield/resume, then we'd just do an inlined
     `Reader.next` call instead. I put this function here to describe why this
     is subtle. *)
  if Reader.is_closed t.reader
  then Reader.next t.reader
  else `Yield
;;

let next_read_operation t =
  match _next_read_operation t with
  | `Error (`Parse _)             -> set_error_and_handle          t `Bad_request; `Close
  | `Error (`Bad_request request) -> set_error_and_handle ~request t `Bad_request; `Close
  | (`Read | `Yield | `Close | `Upgrade) as operation -> operation

let rec read_with_more t bs ~off ~len more =
  let call_handler = Queue.is_empty t.request_queue in
  let consumed = Reader.read_with_more t.reader bs ~off ~len more in
  if is_active t then (
    let reqd = current_reqd_exn t in
    if call_handler then t.request_handler reqd;
    Reqd.flush_request_body reqd);
  (* Keep consuming input as long as progress is made and data is
     available, in case multiple requests were received at once. *)
  if consumed > 0 && consumed < len then
    let off = off + consumed and len = len - consumed in
    consumed + read_with_more t bs ~off ~len more
  else consumed

let read t bs ~off ~len = read_with_more t bs ~off ~len Incomplete
let read_eof t bs ~off ~len = read_with_more t bs ~off ~len Complete

let rec _next_write_operation t =
  if not (is_active t)
  then Writer.next t.writer
  else (
    let reqd = current_reqd_exn t in
    match Reqd.output_state reqd with
    | Waiting ->
      (* XXX(dpatti): I don't think we should need to call this, but it is
         necessary in the case of a streaming, non-chunked body so that you can
         set the appropriate flag. *)
      Reqd.flush_response_body reqd;
      Writer.next t.writer
    | Ready ->
      Reqd.flush_response_body reqd;
      Writer.next t.writer
    | Complete -> _final_write_operation_for t reqd
    | Upgraded ->
      wakeup_reader t;
      (* Even in the Upgrade case, we're still responsible for writing the
         response header, so we might have work to do. *)
      if Writer.has_pending_output t.writer
      then Writer.next t.writer
      else `Upgrade)

and _final_write_operation_for t reqd =
  let next =
    if not (Reqd.persistent_connection reqd) then (
      shutdown_writer t;
      Writer.next t.writer)
    else
      match Reqd.input_state reqd with
      | Waiting -> `Yield
      | Ready -> Writer.next t.writer;
      | Upgraded -> `Upgrade
      | Complete ->
          advance_request_queue t;
          _next_write_operation t
  in
  wakeup_reader t;
  next

let next_write_operation t = _next_write_operation t
let report_write_result t result = Writer.report_result t.writer result
