;; lib/host/static.sx — serve the client kernel + assets so the blog can boot the
;; SX-htmx hypermedia engine (web/engine.sx) and run as a SPA. The native
;; http-listen host reads files with the `file-read` primitive (no perform), so
;; GET /static/** maps to a file under the static root (default "shared/static",
;; resolved against the server cwd — mount ./shared/static there in the container).
;;
;; Also wires the CONTENT-ADDRESSED module cache the SX client expects: GET
;; /sx/h/{hash} serves a web-stack .sxbc by its content hash (immutable, never
;; stale — a deploy changes the content → changes the hash → a fresh URL), and a
;; <script data-sx-manifest> mapping {file -> hash} makes the client's
;; loadBytecodeFile take the content-addressed branch (localStorage + immutable)
;; instead of the path + max-age=3600 branch.
;; Depends on lib/dream/types.sx (dream-response/-html-status/-param) + router.

(define host/static-root "shared/static")
(define host/static-use-root! (fn (r) (set! host/static-root r)))

;; content-type by file extension; default to octet-stream.
(define host/static--ctype
  (fn (path)
    (cond
      ((ends-with? path ".js")    "application/javascript; charset=utf-8")
      ((ends-with? path ".mjs")   "application/javascript; charset=utf-8")
      ((ends-with? path ".css")   "text/css; charset=utf-8")
      ((ends-with? path ".json")  "application/json; charset=utf-8")
      ((ends-with? path ".map")   "application/json; charset=utf-8")
      ((ends-with? path ".svg")   "image/svg+xml")
      ((ends-with? path ".png")   "image/png")
      ((ends-with? path ".woff2") "font/woff2")
      ((ends-with? path ".wasm")  "application/wasm")
      (true                       "application/octet-stream"))))

;; A content-hashed filename (e.g. js_of_ocaml-651f6707.wasm, or anything under
;; /sx/h/) is immutable; everything else gets a modest max-age (mutable bundle).
(define host/static--cache-control
  (fn (rel)
    (if (ends-with? rel ".wasm")
      "public, max-age=31536000, immutable"
      "public, max-age=3600")))

;; reject empty, absolute, or traversal paths.
(define host/static--safe?
  (fn (rel)
    (and (> (len rel) 0)
         (not (starts-with? rel "/"))
         (not (string-contains? rel "..")))))

;; Serve one asset by its path relative to the static root. file-read THROWS on a
;; missing file, so gate on file-exists? first and return a 404 instead.
(define host/static-serve
  (fn (rel)
    (if (not (host/static--safe? rel))
      (dream-html-status 403 "Forbidden")
      (let ((path (str host/static-root "/" rel)))
        (if (not (file-exists? path))
          (dream-html-status 404 "Not Found")
          (dream-response 200
            {:content-type (host/static--ctype rel)
             :cache-control (host/static--cache-control rel)}
            (file-read path)))))))

;; ── content-addressed module cache (/sx/h/{hash}) ───────────────────
;; Each web-stack .sxbc carries its content hash in its head: (sxbc 1 "HASH" ...).
;; Index every .sxbc by that hash at startup so the client can fetch each module
;; immutably + localStorage-cached, and never stale.
(define host/static--sxh->path (dict))   ;; hash -> filepath
(define host/static--file->hash (dict))  ;; "dom.sxbc" -> hash

;; the embedded hash from a .sxbc head: (sxbc 1 "HASH" ...  -> "HASH"
(define host/static--sxbc-hash
  (fn (head) (nth (split head "\"") 1)))

(define host/static-build-sxh-index!
  (fn ()
    (for-each
      (fn (path)
        (let ((h (host/static--sxbc-hash (substr (file-read path) 0 60)))
              (base (last (split path "/"))))
          (dict-set! host/static--sxh->path h path)
          (dict-set! host/static--file->hash base h)))
      (file-glob (str host/static-root "/wasm/sx/*.sxbc")))))

;; GET /sx/h/{hash} -> the .sxbc content, immutable (content-addressed).
(define host/static-sxh-serve
  (fn (hash)
    (let ((path (get host/static--sxh->path hash)))
      (if (nil? path)
        (dream-html-status 404 "Not Found")
        (dream-response 200
          {:content-type "text/sx; charset=utf-8"
           :cache-control "public, max-age=31536000, immutable"}
          (file-read path))))))

;; the data-sx-manifest JSON for the shell: {"modules": {"dom.sxbc": "hash", ...}}.
;; The client's loadBytecodeFile reads manifest.modules[file] -> hash -> /sx/h/.
;; App components the client must eager-load (after the web stack) so their
;; defcomps are registered before a boosted fragment references them. Loaded
;; content-addressed via the modules map below, the same as any web-stack module.
(define host/static--boot-modules (list "relate-picker.sxbc"))

(define host/static-manifest-json
  (fn ()
    (str "{\"v\":1,\"boot\":["
      (join "," (map (fn (m) (str "\"" m "\"")) host/static--boot-modules))
      "],\"defs\":{},\"modules\":{"
      (join ","
        (map (fn (k) (str "\"" k "\":\"" (get host/static--file->hash k) "\""))
          (keys host/static--file->hash)))
      "}}")))

;; Route group: GET /static/** (path) + GET /sx/h/** (content-addressed). A plain
;; route LIST (like host/feed-routes); host/serve combines + flattens the groups.
(define host/static-routes
  (list
    (dream-get "/static/**"
      (fn (req) (host/static-serve (dream-param req "**"))))
    (dream-get "/sx/h/**"
      (fn (req) (host/static-sxh-serve (dream-param req "**"))))))
