;;; http-static-server-sendfile.lsp — hybrid in-memory + sendfile(2)
;;; static server for lumbda.com. Small assets (HTML/CSS/txt) are
;;; cached whole as pre-built HTTP responses and flushed in one
;;; tcp-send. Large assets (PDFs, images) cache only the headers;
;;; the body is streamed from disk via sendfile(2), a zero-copy
;;; kernel->socket path that never enters the lumbda heap.
;;;
;;; Why the split:
;;;   - For small bodies (<= *inline-threshold*), one tcp-send beats
;;;     the syscall split (headers + sendfile).
;;;   - For large bodies, kernel-side DMA + no userspace bounce
;;;     dominates any string copy we could do in-process.
;;;
;;; Configuration:
;;;   *docroot*            filesystem root
;;;   *preload-paths*      URL paths to prepare at startup
;;;   *inline-threshold*   bytes; <= inline, > sendfile
;;;
;;; Target tier: asm-gc (has tcp-sendfile primitive). On tiers that
;;; lack tcp-sendfile (Python, C, bump-only asm) this file will
;;; error at load — it's a deployment example for asm-gc, not a
;;; portable reference.

(define *port*              8080)
(define *docroot*           "www")
(define *cache-max*         100)
(define *max-requests*      1000000)
(define *inline-threshold*  16384)

(define *crlf*              "\r\n")
(define *crlf-crlf*         "\r\n\r\n")

;;; ── MIME ────────────────────────────────────────────────────

(define (ends-with? s suffix)
  (let ((ns (string-length s)) (nf (string-length suffix)))
    (if (< ns nf) #f
        (string=? suffix (substring s (- ns nf) ns)))))

(define (mime-of path)
  (cond
    ((ends-with? path ".html") "text/html; charset=utf-8")
    ((ends-with? path ".css")  "text/css; charset=utf-8")
    ((ends-with? path ".pdf")  "application/pdf")
    ((ends-with? path ".txt")  "text/plain; charset=utf-8")
    ((ends-with? path ".js")   "application/javascript")
    ((ends-with? path ".svg")  "image/svg+xml")
    ((ends-with? path ".png")  "image/png")
    ((ends-with? path ".jpg")  "image/jpeg")
    ((ends-with? path ".ico")  "image/x-icon")
    (else "application/octet-stream")))

;;; ── Response builders ───────────────────────────────────────

(define (inline-response status ctype body)
  (string-append
    "HTTP/1.0 " status *crlf*
    "Content-Type: " ctype *crlf*
    "Content-Length: " (number->string (string-length body)) *crlf*
    "Connection: close" *crlf-crlf*
    body))

(define (headers-only status ctype nbytes)
  (string-append
    "HTTP/1.0 " status *crlf*
    "Content-Type: " ctype *crlf*
    "Content-Length: " (number->string nbytes) *crlf*
    "Connection: close" *crlf-crlf*))

(define (url-to-fs url-path)
  (cond
    ((string=? url-path "/") (string-append *docroot* "/index.html"))
    (else (string-append *docroot* url-path))))

;;; ── Cache ───────────────────────────────────────────────────
;;;
;;; Two tables keyed by URL path:
;;;   *cache-inline*  url -> full HTTP response (string)
;;;   *cache-sendfile* url -> (headers . fs-path) pair
;;;
;;; Per-request handler hits one or the other, never both.

(define *cache-inline*   (make-hash-table))
(define *cache-sendfile* (make-hash-table))

(define (cache-count)
  (+ (hash-table-size *cache-inline*)
     (hash-table-size *cache-sendfile*)))

(define (cache-add! url-path)
  (if (>= (cache-count) *cache-max*)
      (begin
        (display "cache-add!: skipped (cap reached): ")
        (display url-path) (newline)
        'capped)
      (let ((fs-path (url-to-fs url-path)))
        (let ((body (file->string fs-path)))
          (cond
            ((not body)
              (display "cache-add!: missing file: ")
              (display fs-path) (newline)
              'missing)
            ((<= (string-length body) *inline-threshold*)
              (hash-table-set! *cache-inline* url-path
                (inline-response "200 OK" (mime-of fs-path) body))
              'inline)
            (else
              (hash-table-set! *cache-sendfile* url-path
                (cons (headers-only "200 OK" (mime-of fs-path)
                                    (string-length body))
                      fs-path))
              'sendfile))))))

(define *preload-paths*
  (list "/"
        "/style.css"
        "/whitepaper.pdf"
        "/robots.txt"
        "/404.html"))

(define *resp-404*
  (let ((body (file->string (string-append *docroot* "/404.html"))))
    (if body
        (inline-response "404 Not Found" "text/html; charset=utf-8" body)
        (inline-response "404 Not Found" "text/plain" "not found\n"))))

(define (preload-all paths)
  (cond
    ((null? paths) 'done)
    (else
      (cache-add! (car paths))
      (preload-all (cdr paths)))))

(preload-all *preload-paths*)

;;; ── Request parsing ─────────────────────────────────────────

(define SPACE 32)
(define (char-at s i) (char->integer (string-ref s i)))

(define (second-token s)
  (let ((len (string-length s)))
    (let loop1 ((i 0))
      (cond
        ((= i len) "/")
        ((= (char-at s i) SPACE)
         (let loop2 ((j (+ i 1)))
           (cond
             ((= j len) (substring s (+ i 1) len))
             ((= (char-at s j) SPACE) (substring s (+ i 1) j))
             (else (loop2 (+ j 1))))))
        (else (loop1 (+ i 1)))))))

;;; ── Per-request dispatch ────────────────────────────────────

(define (serve client req)
  (let ((path (second-token req)))
    (let ((inline (hash-table-ref/default *cache-inline* path #f)))
      (if inline
          (tcp-send client inline)
          (let ((sf (hash-table-ref/default *cache-sendfile* path #f)))
            (if sf
                (begin
                  (tcp-send     client (car sf))
                  (tcp-sendfile client (cdr sf)))
                (tcp-send client *resp-404*)))))))

;;; ── Main loop ───────────────────────────────────────────────

(define server (tcp-listen *port*))

(define (server-loop n snap)
  (if (>= n *max-requests*)
      (begin
        (display "request cap reached, exiting\n")
        (tcp-close server))
      (begin
        (let ((client (tcp-accept server)))
          (let ((req (tcp-recv client 4096)))
            (if (and req (> (string-length req) 0))
                (serve client req)
                #f))
          (tcp-close client))
        (heap-restore snap)
        (server-loop (+ n 1) snap))))

(display "lumbda-www sendfile on :") (display *port*)
(display " serving ") (display *docroot*)
(display " (") (display (hash-table-size *cache-inline*))
(display " inline, ") (display (hash-table-size *cache-sendfile*))
(display " sendfile, cap ") (display *cache-max*) (display ")")
(newline)
(server-loop 0 (heap-snapshot))
