// =====================================================================
// http.sx — minimal HTTP/1.1 over `std.socket` (subplan 04, Slices 1+2).
//
// The temporary in-repo boundary for the missing `std.http`, written so it
// can be lifted into the sx stdlib later. Deliberately minimal — exactly
// what a JSON API + artifact upload/download server needs:
//
//   * `listen_on(port)` — a listening TCP socket on 0.0.0.0:<port>
//     (INADDR_ANY, so the server is reachable from the LAN);
//   * `read_request` — one full request: request line, header block, and a
//     Content-Length-bounded body, with typed failures for every refusal;
//   * `header_value` — case-insensitive header lookup;
//   * `respond` — one full response: status line, Content-Type/-Length,
//     `Connection: close`, optional extra header lines, body.
//
// REQUEST MEMORY: `read_request` allocates its header and body buffers
// from `context.allocator` — the caller's per-request arena — so every
// view in a `Request` lives exactly as long as the request being served.
//
// LIMITS: the header block must fit HDR_CAP (8K); a body must declare
// Content-Length (411 otherwise) and fit MAX_BODY (413 otherwise). Bodies
// are read whole into memory — streaming uploads are a later slice.
//
// NOT handled (v0, documented): keep-alive (every response closes),
// chunked transfer, TLS (the deployment plan terminates TLS at a reverse
// proxy).
// =====================================================================

#import "modules/std.sx";
sock :: #import "modules/std/socket.sx";

HttpError :: error {
    Socket,
    Bind,
    Listen,
}

// Why a request could not be read. `Closed` = the peer sent nothing
// (speculative preconnect or a dropped connection) — not worth a response.
// Every other tag maps to a 4xx the caller sends.
ReadErr :: error {
    Closed,           // no bytes arrived (idle preconnect / disconnect)
    BadRequest,       // request line unparseable, or the body never finished
    HeadersTooLarge,  // header block exceeds HDR_CAP
    LengthRequired,   // a method with a body arrived without Content-Length
    BodyTooLarge,     // declared Content-Length exceeds MAX_BODY
}

HDR_CAP  :: 8192;
MAX_BODY :: 536870912;   // 512 MiB — bodies are held in memory whole

// One parsed request. All fields are views into per-request arena memory
// owned by `read_request`.
Request :: struct {
    method: string = "";
    path: string = "";
    headers: string = "";   // raw header block, between request line and the blank line
    body: string = "";
}

// Open a listening socket on 0.0.0.0:<port>. SO_REUSEADDR so a restarted
// server can re-bind without waiting out TIME_WAIT.
listen_on :: (port: i64) -> (i32, !HttpError) {
    fd := sock.socket(sock.AF_INET, sock.SOCK_STREAM, 0);
    if fd < 0 { raise error.Socket; }
    opt : i32 = 1;
    sock.setsockopt(fd, sock.SOL_SOCKET, sock.SO_REUSEADDR, @opt, 4);
    addr : sock.SockAddr = .{ sin_len = 16, sin_family = 2, sin_port = sock.htons(port) };
    if sock.bind(fd, @addr, 16) < 0 { sock.close(fd); raise error.Bind; }
    if sock.listen(fd, 16) < 0 { sock.close(fd); raise error.Listen; }
    return fd;
}

SO_RCVTIMEO :i32: 0x1006;   // macOS

// macOS struct timeval (padded to 16 for the setsockopt copy).
Timeval :: struct {
    tv_sec: i64;
    tv_usec: i32 = 0;
    pad: i32 = 0;
}

// Bound blocking reads on `fd` to `secs` seconds. A sequential accept
// loop needs this: browsers open speculative preconnections that never
// send bytes, and an unbounded read on one of those wedges the whole
// server while real requests sit in the backlog.
set_read_timeout :: (fd: i32, secs: i64) {
    tv : Timeval = .{ tv_sec = secs };
    sock.setsockopt(fd, sock.SOL_SOCKET, SO_RCVTIMEO, xx @tv, 16);
}

// Parse the request line `METHOD SP PATH SP HTTP/x.y` off the raw bytes.
// False when the bytes don't look like an HTTP request line.
parse_request :: (raw: string, req: *Request) -> bool {
    i := 0;
    while i < raw.len and raw[i] != 32 { i += 1; }     // 32 = ' '
    if i == 0 or i >= raw.len { return false; }
    req.method = string.{ ptr = raw.ptr, len = i };

    j := i + 1;
    k := j;
    while k < raw.len and raw[k] != 32 and raw[k] != 13 { k += 1; }   // 13 = '\r'
    if k == j or k >= raw.len { return false; }
    if raw[k] != 32 { return false; }                  // no HTTP version after path
    req.path = string.{ ptr = @raw[j], len = k - j };
    return true;
}

// Case-insensitive ASCII equality for header names.
hname_eq :: (a: string, b: string) -> bool {
    if a.len != b.len { return false; }
    i := 0;
    while i < a.len {
        ca := a[i];
        cb := b[i];
        if ca >= 65 and ca <= 90 { ca += 32; }   // 'A'..'Z' -> lower
        if cb >= 65 and cb <= 90 { cb += 32; }
        if ca != cb { return false; }
        i += 1;
    }
    return true;
}

// The value of header `name` (case-insensitive) in a raw header block —
// leading spaces/tabs trimmed, null when absent.
header_value :: (headers: string, name: string) -> ?string {
    i := 0;
    while i < headers.len {
        // the current line: [i, eol)
        eol := i;
        while eol < headers.len and headers[eol] != 13 { eol += 1; }   // 13 = '\r'
        // split at ':'
        c := i;
        while c < eol and headers[c] != 58 { c += 1; }                 // 58 = ':'
        if c < eol {
            nm := string.{ ptr = @headers[i], len = c - i };
            if hname_eq(nm, name) {
                v := c + 1;
                while v < eol and (headers[v] == 32 or headers[v] == 9) { v += 1; }
                return string.{ ptr = @headers[v], len = eol - v };
            }
        }
        // skip "\r\n"
        i = eol + 2;
    }
    return null;
}

// Non-negative decimal, or null (empty/garbage/overflow-length input).
parse_content_length :: (s: string) -> ?i64 {
    if s.len == 0 or s.len > 12 { return null; }
    v : i64 = 0;
    i := 0;
    while i < s.len {
        c := s[i];
        if c < 48 or c > 57 { return null; }   // '0'..'9'
        v = v * 10 + (c - 48);
        i += 1;
    }
    return v;
}

// Find "\r\n\r\n" in buf[0..len); -1 when absent.
find_blank_line :: (buf: [*]u8, len: i64) -> i64 {
    i : i64 = 0;
    while i + 3 < len {
        if buf[i] == 13 and buf[i+1] == 10 and buf[i+2] == 13 and buf[i+3] == 10 {
            return i;
        }
        i += 1;
    }
    return -1;
}

// Read one full request off `client`: request line + headers (up to
// HDR_CAP), then — when Content-Length says so — the whole body. GETs
// carry no body; any request DECLARING a body gets it read regardless of
// method, so the router can 405 with the connection drained.
read_request :: (client: i32, req: *Request) -> !ReadErr {
    hbuf : [*]u8 = xx context.allocator.alloc_bytes(HDR_CAP);
    filled : i64 = 0;
    hdr_end : i64 = -1;

    while hdr_end < 0 {
        if filled >= HDR_CAP { raise error.HeadersTooLarge; }
        n := sock.read(client, @hbuf[filled], xx (HDR_CAP - filled));
        if n <= 0 {
            if filled == 0 { raise error.Closed; }
            raise error.BadRequest;
        }
        filled += n;
        hdr_end = find_blank_line(hbuf, filled);
    }

    head := string.{ ptr = hbuf, len = hdr_end };
    if !parse_request(head, req) { raise error.BadRequest; }

    // header block = after the request line, before the blank line
    line_end := 0;
    while line_end < head.len and head[line_end] != 13 { line_end += 1; }
    hstart := line_end + 2;
    if hstart < hdr_end {
        req.headers = string.{ ptr = @hbuf[hstart], len = hdr_end - hstart };
    } else {
        req.headers = "";
    }

    // body: Content-Length governs; absent means none expected — except
    // for POST/PUT, which must declare one (411) so an upload can never
    // be silently truncated.
    clq := header_value(req.headers, "content-length");
    if clq == null {
        if req.method == "POST" or req.method == "PUT" { raise error.LengthRequired; }
        req.body = "";
        return;
    }
    clv := parse_content_length(clq!);
    if clv == null { raise error.BadRequest; }
    body_len := clv!;
    if body_len == 0 { req.body = ""; return; }
    if body_len > MAX_BODY { raise error.BodyTooLarge; }

    bbuf : [*]u8 = xx context.allocator.alloc_bytes(xx body_len);
    have : i64 = 0;

    // bytes that arrived in the header read belong to the body
    spill := filled - (hdr_end + 4);
    if spill > 0 {
        take := if spill > body_len then body_len else spill;
        memcpy(bbuf, @hbuf[hdr_end + 4], xx take);
        have = take;
    }
    while have < body_len {
        n := sock.read(client, @bbuf[have], xx (body_len - have));
        if n <= 0 { raise error.BadRequest; }   // peer quit mid-body
        have += n;
    }
    req.body = string.{ ptr = bbuf, len = body_len };
    return;
}

status_text :: (code: i64) -> string {
    if code == 200 { return "OK"; }
    if code == 400 { return "Bad Request"; }
    if code == 401 { return "Unauthorized"; }
    if code == 403 { return "Forbidden"; }
    if code == 404 { return "Not Found"; }
    if code == 405 { return "Method Not Allowed"; }
    if code == 409 { return "Conflict"; }
    if code == 411 { return "Length Required"; }
    if code == 413 { return "Payload Too Large"; }
    if code == 503 { return "Service Unavailable"; }
    return "Internal Server Error";
}

// Write one complete response and leave the connection for the caller to
// close. `extra` is zero or more pre-formatted header lines, each ending
// in `\r\n` ("" for none).
respond :: (client: i32, code: i64, content_type: string, extra: string, body: string) {
    h := concat("HTTP/1.1 ", concat(int_to_string(code), concat(" ", status_text(code))));
    h = concat(h, "\r\n");
    h = concat(h, concat("Content-Type: ", concat(content_type, "\r\n")));
    h = concat(h, concat("Content-Length: ", concat(int_to_string(body.len), "\r\n")));
    h = concat(h, "Connection: close\r\n");
    h = concat(h, extra);
    h = concat(h, "\r\n");
    sock.write(client, h.ptr, xx h.len);
    if body.len > 0 { sock.write(client, body.ptr, xx body.len); }
}
