// =====================================================================
// http_client.sx — minimal HTTP/1.1 client over `std.socket` (subplan 03,
// Slice 3): the temporary in-repo twin of the server shim in
// src/server/http.sx, liftable into the sx stdlib alongside it.
//
// Exactly what remote publish needs: POST one body to one path on one
// server, with a bearer credential, and get the status + JSON body back.
//
// `--server` URL boundary (v0, loud): `http://<ipv4-or-localhost>:<port>`.
// No DNS (getaddrinfo) and no TLS — `https://` is refused with a message
// pointing at the reverse-proxy deployment story; LAN/NAS targets are
// dotted quads or localhost.
//
// The response is read to EOF — distd always answers `Connection: close`
// — into a fixed RESP_CAP buffer (responses are JSON, never artifacts),
// then split at the blank line into status code + body.
// =====================================================================

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

netc :: #library "c";
c_connect :: (fd: i32, addr: *sock.SockAddr, addrlen: u32) -> i32 #foreign netc "connect";

RESP_CAP :: 262144;

ClientErr :: error {
    BadUrl,       // not http://<ipv4-or-localhost>:<port>
    Tls,          // https:// — unsupported by design (proxy terminates)
    Connect,      // socket/connect failed
    Send,         // write failed mid-request
    BadResponse,  // EOF before a parseable status line + blank line
}

// A parsed `--server` target.
ServerUrl :: struct {
    host: string = "";    // as written (for the Host: header)
    addr: u32 = 0;        // IPv4, network byte order
    port: i64 = 80;
}

// One response, body as a view into client-allocated memory.
HttpResponse :: struct {
    status: i64 = 0;
    body: string = "";
}

// Dotted-quad IPv4 (or "localhost") -> sin_addr in network byte order.
ipv4_addr :: (host: string) -> ?u32 {
    if host == "localhost" { return 0x0100007F; }   // 127.0.0.1
    octets : [4]i64 = .[ -1, -1, -1, -1 ];
    oi := 0;
    v : i64 = -1;
    i := 0;
    while i < host.len {
        c := host[i];
        if c == 46 {   // '.'
            if v < 0 or oi >= 3 { return null; }
            octets[oi] = v;
            oi += 1;
            v = -1;
        } else {
            if c < 48 or c > 57 { return null; }
            if v < 0 { v = 0; }
            v = v * 10 + (c - 48);
            if v > 255 { return null; }
        }
        i += 1;
    }
    if v < 0 or oi != 3 { return null; }
    octets[3] = v;
    // network byte order = a,b,c,d in memory = little-endian a|b<<8|c<<16|d<<24
    return cast(u32) (octets[0] | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24));
}

// Parse `http://<host>:<port>` (port required; host ipv4 or localhost).
parse_server_url :: (url: string) -> (ServerUrl, !ClientErr) {
    HTTPS :: "https://";
    HTTP  :: "http://";
    if url.len >= 8 {
        h := string.{ ptr = url.ptr, len = 8 };
        if h == HTTPS { raise error.Tls; }
    }
    if url.len <= 7 { raise error.BadUrl; }
    p := string.{ ptr = url.ptr, len = 7 };
    if p != HTTP { raise error.BadUrl; }

    rest := string.{ ptr = @url[7], len = url.len - 7 };
    // split host:port (no path component allowed)
    ci := -1;
    i := 0;
    while i < rest.len {
        if rest[i] == 47 { raise error.BadUrl; }   // '/' — no path in --server
        if rest[i] == 58 { ci = i; }               // ':'
        i += 1;
    }
    if ci <= 0 or ci >= rest.len - 1 { raise error.BadUrl; }
    host := string.{ ptr = rest.ptr, len = ci };
    pstr := string.{ ptr = @rest[ci + 1], len = rest.len - ci - 1 };

    port : i64 = 0;
    j := 0;
    while j < pstr.len {
        c := pstr[j];
        if c < 48 or c > 57 { raise error.BadUrl; }
        port = port * 10 + (c - 48);
        j += 1;
    }
    if port < 1 or port > 65535 { raise error.BadUrl; }

    aq := ipv4_addr(host);
    if aq == null { raise error.BadUrl; }
    return ServerUrl.{ host = host, addr = aq!, port = port };
}

// Write all of `data` to `fd`.
send_all :: (fd: i32, data: string) -> !ClientErr {
    off : i64 = 0;
    while off < data.len {
        n := sock.write(fd, @data[off], xx (data.len - off));
        if n <= 0 { raise error.Send; }
        n2 : i64 = xx n;
        off += n2;
    }
    return;
}

// POST `body` to `path` on `srv`. `bearer` "" sends no Authorization
// header. The response buffer comes from `context.allocator`.
http_post :: (srv: ServerUrl, path: string, bearer: string, content_type: string, body: string) -> (HttpResponse, !ClientErr) {
    fd := sock.socket(sock.AF_INET, sock.SOCK_STREAM, 0);
    if fd < 0 { raise error.Connect; }
    addr : sock.SockAddr = .{
        sin_len = 16, sin_family = 2,
        sin_port = sock.htons(srv.port), sin_addr = srv.addr,
    };
    if c_connect(fd, @addr, 16) < 0 { sock.close(fd); raise error.Connect; }
    hs.set_read_timeout(fd, 10);

    h := concat("POST ", concat(path, " HTTP/1.1\r\n"));
    h = concat(h, concat("Host: ", concat(srv.host, "\r\n")));
    if bearer.len > 0 {
        h = concat(h, concat("Authorization: Bearer ", concat(bearer, "\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\r\n");

    serr := false;
    send_all(fd, h) catch { serr = true; };
    if !serr { send_all(fd, body) catch { serr = true; }; }
    if serr { sock.close(fd); raise error.Send; }

    // Read to EOF (the server closes after one response).
    buf : [*]u8 = xx context.allocator.alloc_bytes(RESP_CAP);
    filled : i64 = 0;
    while filled < RESP_CAP {
        n := sock.read(fd, @buf[filled], xx (RESP_CAP - filled));
        if n <= 0 { break; }
        n2 : i64 = xx n;
        filled += n2;
    }
    sock.close(fd);

    raw := string.{ ptr = buf, len = filled };
    return parse_response(raw);
}

// Split a raw response into status code + body.
parse_response :: (raw: string) -> (HttpResponse, !ClientErr) {
    // status: the integer after the first space ("HTTP/1.1 200 OK")
    sp := -1;
    i := 0;
    while i < raw.len {
        if raw[i] == 32 { sp = i; break; }
        i += 1;
    }
    if sp < 0 or sp + 4 > raw.len { raise error.BadResponse; }
    status : i64 = 0;
    j := sp + 1;
    digits := 0;
    while j < raw.len {
        c := raw[j];
        if c < 48 or c > 57 { break; }
        status = status * 10 + (c - 48);
        digits += 1;
        j += 1;
    }
    if digits != 3 { raise error.BadResponse; }

    // body: after the first blank line
    k := 0;
    body := "";
    found := false;
    while k + 3 < raw.len {
        if raw[k] == 13 and raw[k+1] == 10 and raw[k+2] == 13 and raw[k+3] == 10 {
            start := k + 4;
            body = string.{ ptr = @raw[start], len = raw.len - start };
            found = true;
            break;
        }
        k += 1;
    }
    if !found { raise error.BadResponse; }
    return HttpResponse.{ status = status, body = body };
}
