// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// A request as it goes on the wire, whichever version carries it.
priv struct Outgoing {
  verb : @method.Method
  scheme : String
  authority : String
  target : String
  fields : Array[@header.Header]
  payload : Payload
}

///|
/// A response as it comes off the wire, and whether its connection may carry another.
priv struct Reply {
  status : Int32
  reason : String
  version : String
  headers : Array[(String, String)]
  body : Bytes
  reusable : Bool
}

///|
fn ascii(fields : Array[@header.Header]) -> Array[(String, String)] {
  fields.map(f => {
    let (name, value) = f.ascii()
    (name.to_lower(), value)
  })
}

///|
/// A response body being read from an HTTP/1.1 connection, a piece at a time.
priv struct H1 {
  conn : @tcp.Conn
  wire : @http1.Conn
  mut done : Bool
  mut reusable : Bool
}

///|
/// Send the request and read the response head; the body is left on `H1`.
async fn h1_open(conn : @tcp.Conn, out : Outgoing) -> (Reply, H1) {
  let wire = @http1.Conn::new(Client)
  let length = out.payload.length()
  // RFC 9110 ยง8.6: a method that defines a body says how long it is, even when empty.
  let framing : @http1.Body = match length {
    Some(n) =>
      if n > 0 || out.verb is (Post | Put | Patch) {
        Sized(n.to_int())
      } else {
        Empty
      }
    None => Chunked
  }
  // The framing decides `content-length` here; one carried in from the client would be a
  // second, possibly different, value.
  let fields = out.fields.filter(f => f.ascii().0.to_lower() != "content-length")
  let head = @http1.Request::of(
    out.verb.name(),
    out.target,
    fields~,
    host=out.authority,
    body=framing,
  )
  let sent = Buffer()
  sent.write_bytes(wire.send(Request(head)))
  match out.payload {
    Whole(bytes) =>
      if bytes.length() > 0 {
        sent.write_bytes(wire.send(Data(bytes)))
      }
    Pieces(source) =>
      while source.next() is Some(piece) {
        if piece.length() > 0 {
          sent.write_bytes(wire.send(Data(piece)))
        }
      }
  }
  sent.write_bytes(wire.send(End([])))
  conn.write(sent.to_bytes())
  let reader = { conn, wire, done: false, reusable: false, }
  let reply = {
    status: 0,
    reason: "",
    version: "HTTP/1.1",
    headers: [],
    body: b"",
    reusable: false,
  }
  match reader.head(reply) {
    Some(head) => (head, reader)
    None => raise @http1.Partial
  }
}

///|
/// Read up to the response head, skipping informational answers. `None` means the connection
/// closed first.
async fn H1::head(self : H1, _ : Reply) -> Reply? {
  for ;; {
    match self.wire.next() {
      NeedData =>
        match self.conn.read_some(max_len=chunk) {
          Some(data) => self.wire.receive(data[:])
          None => self.wire.receive(b"")
        }
      Response(r) => {
        // A response that ends its message in the head: the connection may carry another.
        let empty = self.wire.theirs() is Done
        self.done = empty
        self.reusable = empty && self.wire.ours() is Done
        return Some({
          status: r.status,
          reason: r.reason,
          version: r.version,
          headers: ascii(r.fields),
          body: b"",
          reusable: false,
        })
      }
      Closed => return None
      Informational(_) | Data(_) | End(_) | Paused | Request(_) => ()
    }
  }
}

///|
/// The next piece of the body, trailers at the end, `None` once it has all been read.
async fn H1::next(self : H1) -> Chunk? {
  for ;; {
    match self.wire.next() {
      Data(piece) => return Some(Data(piece))
      End(trailers) => {
        self.done = true
        self.reusable = self.wire.ours() is Done &&
          self.wire.theirs() is Done &&
          self.wire.trailing().length() == 0
        return Some(End(ascii(trailers)))
      }
      Closed => {
        self.done = true
        self.reusable = false
        // A body that ran to the close is complete when the framing said so; anything else cut
        // short is an error, and the state machine reports nothing else here.
        if self.wire.theirs() is Done {
          return Some(End([]))
        }
        raise @http1.Partial
      }
      NeedData =>
        match self.conn.read_some(max_len=chunk) {
          Some(data) => self.wire.receive(data[:])
          None => self.wire.receive(b"")
        }
      Informational(_) | Response(_) | Paused | Request(_) => ()
    }
  }
}

///|
/// Read the rest of the body, refusing to hold more than `limit` octets.
async fn H1::collect(self : H1, limit : Int32) -> Bytes {
  let out = Buffer()
  let mut got = 0
  for ;; {
    match self.next() {
      Some(Data(piece)) => {
        got += piece.length()
        if got > limit {
          raise Exceeded(limit~, got~)
        }
        out.write_bytes(piece)
      }
      Some(End(_)) | None => break
    }
  }
  out.to_bytes()
}

///|
/// One exchange over HTTP/1.1, framed and checked by moonhttp's state machine.
async fn h1(conn : @tcp.Conn, out : Outgoing, limit : Int32) -> Reply {
  let (head, reader) = h1_open(conn, out)
  let body = reader.collect(limit)
  { ..head, body, reusable: reader.reusable, }
}