;; lib/dream/static.sx — Dream-on-SX static file serving.
;; dream-static mounts at a ** route and serves files under a root: content-type by
;; extension, ETags + If-None-Match (304), and Range requests (206). The filesystem
;; is injectable: production reads via (perform op); tests pass an in-memory map.
;; Depends on types.sx.

;; ── filesystem backends ────────────────────────────────────────────
;; An fs is (fn (op) result); op {:op "file/read" :path p} -> content | nil.
(define dream-static-perform-fs (fn (op) (perform op)))

;; in-memory fs over a {path -> content} dict (tests + demos)
(define
  dream-memory-fs
  (fn
    (files)
    (fn
      (op)
      (if (= (get op :op) "file/read") (get files (get op :path)) nil))))

;; ── content-type by extension ──────────────────────────────────────
(define dr/mime-types {:js "application/javascript" :jpeg "image/jpeg" :css "text/css; charset=utf-8" :ico "image/x-icon" :mjs "application/javascript" :html "text/html; charset=utf-8" :pdf "application/pdf" :jpg "image/jpeg" :json "application/json" :htm "text/html; charset=utf-8" :wasm "application/wasm" :webp "image/webp" :gif "image/gif" :png "image/png" :svg "image/svg+xml" :md "text/markdown; charset=utf-8" :xml "application/xml" :sx "text/plain; charset=utf-8" :txt "text/plain; charset=utf-8"})

(define
  dr/ext-of
  (fn
    (path)
    (let
      ((segs (split path ".")))
      (if
        (> (len segs) 1)
        (lower (nth segs (- (len segs) 1)))
        ""))))

(define
  dream-content-type-for
  (fn
    (path)
    (or (get dr/mime-types (dr/ext-of path)) "application/octet-stream")))

;; ── ETag (weak content hash) ───────────────────────────────────────
(define
  dr/static-hash
  (fn (s) (dr/static-hash-loop s 0 (string-length s) 7)))
(define
  dr/static-hash-loop
  (fn
    (s i n h)
    (if
      (>= i n)
      h
      (dr/static-hash-loop
        s
        (+ i 1)
        n
        (mod (+ (* h 131) (char-code (char-at s i))) 2147483647)))))
(define
  dr/etag-of
  (fn
    (content)
    (str "\"" (dr/static-hash content) "-" (string-length content) "\"")))
(define
  dr/etag-match?
  (fn (inm etag) (and (not (nil? inm)) (or (= inm "*") (= inm etag)))))

;; ── path safety ────────────────────────────────────────────────────
(define
  dr/static-relpath
  (fn
    (req)
    (or (dream-param req "**") (substr (dream-path req) 1))))
(define
  dr/unsafe-path?
  (fn (rel) (or (contains? rel "..") (starts-with? rel "/")))) 
(define
  dr/path-join
  (fn
    (root rel)
    (if (ends-with? root "/") (str root rel) (str root "/" rel))))

;; ── range requests ─────────────────────────────────────────────────
(define
  dr/parse-range
  (fn
    (header total)
    (let
      ((eq (index-of header "=")))
      (if
        (< eq 0)
        nil
        (let
          ((spec (substr header (+ eq 1))))
          (let
            ((dash (index-of spec "-")))
            (if
              (< dash 0)
              nil
              (let
                ((s (substr spec 0 dash))
                  (e (substr spec (+ dash 1))))
                (let
                  ((start (if (= s "") 0 (parse-int s)))
                    (end (if (= e "") (- total 1) (parse-int e))))
                  (if
                    (or
                      (< start 0)
                      (>= start total)
                      (> end (- total 1))
                      (> start end))
                    nil
                    {:start start :end end}))))))))))

(define
  dr/serve-range
  (fn
    (req content etag ctype)
    (let
      ((total (string-length content)))
      (let
        ((r (dr/parse-range (dream-header req "range") total)))
        (if
          (nil? r)
          (dream-add-header
            (dream-response 416 {:content-type ctype} "")
            "content-range"
            (str "bytes */" total))
          (let
            ((start (get r :start)) (end (get r :end)))
            (dream-add-header
              (dream-add-header
                (dream-response
                  206
                  {:content-type ctype}
                  (substr content start (+ 1 (- end start))))
                "content-range"
                (str "bytes " start "-" end "/" total))
              "etag"
              etag)))))))

;; ── serving ────────────────────────────────────────────────────────
(define
  dr/serve-file
  (fn
    (req content)
    (let
      ((rel (dr/static-relpath req)))
      (let
        ((etag (dr/etag-of content)) (ctype (dream-content-type-for rel)))
        (cond
          ((dr/etag-match? (dream-header req "if-none-match") etag)
            (dream-add-header (dream-empty 304) "etag" etag))
          ((dream-header req "range")
            (dr/serve-range req content etag ctype))
          (else
            (dream-add-header
              (dream-add-header
                (dream-response 200 {:content-type ctype} content)
                "etag"
                etag)
              "accept-ranges"
              "bytes")))))))

(define
  dream-static-with
  (fn
    (root fs)
    (fn
      (req)
      (let
        ((rel (dr/static-relpath req)))
        (if
          (dr/unsafe-path? rel)
          (dream-html-status 403 "Forbidden")
          (let
            ((content (fs {:path (dr/path-join root rel) :op "file/read"})))
            (if
              (nil? content)
              (dream-not-found)
              (dr/serve-file req content))))))))

(define
  dream-static
  (fn (root) (dream-static-with root dream-static-perform-fs)))
