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

// What a request looks like once it leaves the caller: the semicolons are gone, and what is left
// is a head and a body. The pieces here are the ones both hosts agree on — the content type a
// body brings, the length it declares, the two fields every request carries — and the encoders
// that turn them into HTTP/1.1 octets and HTTP/2 frames.

///|
/// 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
}

///|
/// How a request would go on the wire: the version that would carry it, and the pieces it is
/// written as, in order.
///
/// The pieces of an HTTP/1.1 request are its head and then its body; those of an HTTP/2 one are
/// the connection preface, this side's settings, and then one entry per frame, labelled with the
/// frame's type and the flags it carries.
pub(all) struct Wire {
  version : String
  pieces : Array[(String, Bytes)]
}

///|
/// The headers a request goes out with before a transport adds what only it knows: the content
/// type the body brings, the length it declares, and the two fields every request carries.
fn prepared(
  body : Body,
  fields : Array[(String, String)],
) -> (Array[(String, String)], Payload) {
  let (payload, kind) = body.payload()
  let all = fields.copy()
  if kind is Some(k) && !has(all, "content-type") {
    all.push(("content-type", k))
  }
  if payload.length() is Some(n) && !has(all, "content-length") {
    all.push(("content-length", n.to_string()))
  }
  if !has(all, "user-agent") {
    all.push(("user-agent", "moonfetch"))
  }
  if !has(all, "accept") {
    all.push(("accept", "*/*"))
  }
  (all, payload)
}

///|
/// A request as the transports want it: the target is the path and query, the authority is what
/// goes in `host` or `:authority`.
fn going(
  verb : @method.Method,
  url : @moonurl.Url,
  fields : Array[(String, String)],
  payload : Payload,
) -> Outgoing {
  let origin = Origin::of(url)
  let path = if url.path == "" { "/" } else { url.path }
  let target = match url.query {
    Some(query) => path + "?" + query
    None => path
  }
  {
    verb,
    scheme: origin.scheme,
    authority: origin.authority(),
    target,
    fields: fields.map(field => @header.Header::of(field.0, field.1)),
    payload,
  }
}

///|
/// A whole request as one value, the way a client with no jar, proxy or codings would write it.
fn as_prepared(
  verb : @method.Method,
  url : StringView,
  query : Array[(String, String)]?,
  headers : Array[(String, String)]?,
  body : Body,
) -> Outgoing raise Refused {
  let target = located(None, url, query)
  let (fields, payload) = prepared(body, headers.unwrap_or([]))
  going(verb, target, fields, payload)
}

///|
/// The octets an HTTP/1.1 request is written as, for a caller that wants to see them: the head
/// with its framing, then the body.
///
/// A body produced while the request goes out is not read here — taking its pieces would spend
/// them — so only the head is written, with the framing that says a body follows.
///
/// `Refused::Url` for a URL no request can name, and the state machine's own refusal for a field
/// it cannot write.
pub fn wire(
  verb : @method.Method,
  url : StringView,
  query? : Array[(String, String)],
  headers? : Array[(String, String)],
  body? : Body,
) -> Wire raise {
  let out = as_prepared(verb, url, query, headers, body.unwrap_or(Empty))
  { version: "HTTP/1.1", pieces: h1_pieces(@http1.Conn::new(Client), out), }
}

///|
/// The frames an HTTP/2 request is written as: the connection preface, this side's settings, and
/// the HEADERS and DATA frames of the exchange.
///
/// The connection is a fresh one, so the field section is encoded against the static table alone,
/// which is what a first request on a connection looks like.
pub fn frames(
  verb : @method.Method,
  url : StringView,
  query? : Array[(String, String)],
  headers? : Array[(String, String)],
  body? : Body,
) -> Wire raise {
  let out = as_prepared(verb, url, query, headers, body.unwrap_or(Empty))
  { version: "HTTP/2", pieces: h2_pieces(out), }
}

///|
/// The head an HTTP/1.1 request is written as, framing included: the request line, the fields, and
/// the line the framing needs.
fn h1_head(state : @http1.Conn, out : Outgoing) -> Bytes raise {
  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 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,
  )
  state.send(Request(head))
}

///|
/// The octets of an HTTP/1.1 request, piece by piece: the head, the body, and the empty line that
/// ends a chunked one.
fn h1_pieces(
  state : @http1.Conn,
  out : Outgoing,
) -> Array[(String, Bytes)] raise {
  let pieces : Array[(String, Bytes)] = [("request head", h1_head(state, out))]
  match out.payload {
    Whole(bytes) =>
      if bytes.length() > 0 {
        pieces.push(("body", state.send(Data(bytes))))
      }
    Pieces(_) => ()
  }
  let end = state.send(End([]))
  if end.length() > 0 {
    pieces.push(("end of body", end))
  }
  pieces
}

///|
/// The connection preface of RFC 9113 §3.4.
let preface : Bytes = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"

///|
/// The octets of an HTTP/2 request, cut into frames: the preface and this side's settings, then
/// the exchange's own.
fn h2_pieces(out : Outgoing) -> Array[(String, Bytes)] raise {
  let state = @http2.Conn::new(Client)
  let id = state.next_stream()
  let fields : Array[@header.Header] = [
    @header.Header::of(":method", out.verb.name()),
    @header.Header::of(":scheme", out.scheme),
    @header.Header::of(":authority", out.authority),
    @header.Header::of(":path", out.target),
  ]
  for f in out.fields {
    let (name, value) = f.ascii()
    let lower = name.to_lower()
    if h2_carries(lower) {
      fields.push(@header.Header::of(lower, value))
    }
  }
  let empty = out.payload.length() is Some(0)
  state.headers(id, fields, end=empty)
  match out.payload {
    Whole(bytes) => {
      let mut at = 0
      while at < bytes.length() {
        let room = state.window(id)
        if room <= 0 {
          break
        }
        let n = if room < bytes.length() - at {
          room
        } else {
          bytes.length() - at
        }
        state.data(id, bytes[at:at + n])
        at += n
      }
      if !empty {
        state.end(id)
      }
    }
    Pieces(_) => if !empty { state.end(id) }
  }
  cut(state.take())
}

///|
/// Whether HTTP/2 carries this field: the connection-specific ones it forbids (RFC 9113 §8.2.2)
/// belong to HTTP/1.1, and the authority took `host`'s place.
fn h2_carries(name : String) -> Bool {
  !(name
  is ("connection"
  | "keep-alive"
  | "proxy-connection"
  | "transfer-encoding"
  | "upgrade"
  | "host"))
}

///|
/// Cut a connection's octets into the pieces a reader can name: the preface, then each frame with
/// its type and flags.
fn cut(bytes : Bytes) -> Array[(String, Bytes)] {
  let pieces : Array[(String, Bytes)] = []
  let mut at = 0
  if bytes.length() >= preface.length() && bytes[0:preface.length()] == preface {
    pieces.push(("connection preface", preface))
    at = preface.length()
  }
  while at + 9 <= bytes.length() {
    let length = (bytes[at].to_int() << 16) |
      (bytes[at + 1].to_int() << 8) |
      bytes[at + 2].to_int()
    let end = at + 9 + length
    if end > bytes.length() {
      break
    }
    pieces.push(
      (
        frame_label(bytes[at + 3].to_int(), bytes[at + 4].to_int()),
        bytes[at:end].to_owned(),
      ),
    )
    at = end
  }
  if at < bytes.length() {
    pieces.push(("trailing octets", bytes[at:].to_owned()))
  }
  pieces
}

///|
/// A frame's name, with the flags a reader cares about: end of stream, end of headers, an
/// acknowledgement (RFC 9113 §6).
fn frame_label(kind : Int32, flags : Int32) -> String {
  let name = match kind {
    0 => "DATA"
    1 => "HEADERS"
    2 => "PRIORITY"
    3 => "RST_STREAM"
    4 => "SETTINGS"
    5 => "PUSH_PROMISE"
    6 => "PING"
    7 => "GOAWAY"
    8 => "WINDOW_UPDATE"
    9 => "CONTINUATION"
    _ => "frame \{kind}"
  }
  let end_stream : Int32 = 1
  let end_headers : Int32 = 4
  let mut suffix = ""
  if (flags & end_stream) != 0 && (kind == 0 || kind == 1) {
    suffix = " + END_STREAM"
  } else if (flags & end_headers) != 0 && kind == 1 {
    suffix = " + END_HEADERS"
  } else if (flags & end_stream) != 0 && kind == 4 {
    suffix = " + ACK"
  }
  name + suffix
}