;; lib/dream/middleware.sx — Dream-on-SX middleware.
;; A middleware is handler->handler. Composition is plain function composition:
;; m1 @@ m2 @@ handler = (m1 (m2 handler)). Depends on types.sx + router.sx
;; (reuses dr/apply-middlewares for the fold).

;; ── composition ────────────────────────────────────────────────────
;; (dream-pipeline (list m1 m2 m3) handler) = (m1 (m2 (m3 handler))).
(define
  dream-pipeline
  (fn (middlewares handler) (dr/apply-middlewares middlewares handler)))

;; identity middleware
(define dream-no-middleware (fn (next) next))

;; ── logger ─────────────────────────────────────────────────────────
;; Parameterised on a clock and a sink so it is testable without IO.
;; sink receives {:method :path :status :elapsed}.
(define
  dream-logger-with
  (fn
    (clock sink)
    (fn
      (next)
      (fn
        (req)
        (let
          ((t0 (clock)))
          (let ((resp (next req))) (begin (sink {:path (dream-path req) :status (dream-status resp) :method (dream-method req) :elapsed (- (clock) t0)}) resp)))))))

;; default logger performs host effects for the clock and the log sink
(define
  dream-logger
  (dream-logger-with
    (fn () (perform (:dream-clock)))
    (fn (entry) (perform (:dream-log entry)))))

;; format a log entry as a one-line string (apache-ish)
(define
  dream-log-line
  (fn
    (entry)
    (str
      (get entry :method)
      " "
      (get entry :path)
      " -> "
      (get entry :status)
      " ("
      (get entry :elapsed)
      "ms)")))

;; ── content-type sniffer ───────────────────────────────────────────
(define
  dr/sniff-content-type
  (fn
    (body)
    (cond
      ((= body "") "text/plain; charset=utf-8")
      ((starts-with? body "<") "text/html; charset=utf-8")
      ((starts-with? body "{") "application/json")
      ((starts-with? body "[") "application/json")
      (else "text/plain; charset=utf-8"))))

;; sets Content-Type from the body only when the handler left it unset
(define
  dream-content-type
  (fn
    (next)
    (fn
      (req)
      (let
        ((resp (next req)))
        (if
          (dream-resp-header resp "content-type")
          resp
          (dream-add-header
            resp
            "content-type"
            (dr/sniff-content-type (dream-resp-body resp))))))))

;; ── small reusable middlewares ─────────────────────────────────────
;; always attach a response header
(define
  dream-set-header
  (fn
    (name val)
    (fn (next) (fn (req) (dream-add-header (next req) name val)))))

;; rewrite/observe the request before the handler sees it
(define
  dream-tap-request
  (fn (f) (fn (next) (fn (req) (next (f req))))))
