;; lib/dream/router.sx — Dream-on-SX routing.
;; Routes are dicts {:method :path :handler}; a router is a handler that
;; dispatches request -> response by method + path, extracting :name path
;; params and binding a ** catch-all. No path match -> 404; path matches but
;; method doesn't -> 405 + Allow. HEAD falls back to the GET handler with an
;; empty body. Depends on types.sx.

;; ── route constructors (one per HTTP method) ───────────────────────
(define dream-get (fn (path handler) (dream-route "GET" path handler)))
(define dream-post (fn (path handler) (dream-route "POST" path handler)))
(define dream-put (fn (path handler) (dream-route "PUT" path handler)))
(define
  dream-delete
  (fn (path handler) (dream-route "DELETE" path handler)))
(define dream-patch (fn (path handler) (dream-route "PATCH" path handler)))
(define dream-head (fn (path handler) (dream-route "HEAD" path handler)))
(define
  dream-options
  (fn (path handler) (dream-route "OPTIONS" path handler)))
(define dream-any (fn (path handler) (dream-route "ANY" path handler)))

;; ── path segmentation ──────────────────────────────────────────────
;; "/users/42/" -> ("users" "42"); "/" -> ()
(define
  dr/segs
  (fn (path) (filter (fn (s) (not (= s ""))) (split path "/"))))

(define
  dr/join-path
  (fn
    (prefix path)
    (str "/" (join "/" (concat (dr/segs prefix) (dr/segs path))))))

;; ── segment matching ───────────────────────────────────────────────
;; Returns a params dict on match (possibly empty {}), nil on no match.
(define
  dr/match-segs
  (fn
    (pat path params)
    (cond
      ((and (empty? pat) (empty? path)) params)
      ((empty? pat) nil)
      (else
        (let
          ((ps (first pat)))
          (cond
            ((= ps "**") (assoc params "**" (join "/" path)))
            ((empty? path) nil)
            ((starts-with? ps ":")
              (dr/match-segs
                (rest pat)
                (rest path)
                (assoc params (substr ps 1) (first path))))
            ((= ps (first path))
              (dr/match-segs (rest pat) (rest path) params))
            (else nil)))))))

;; path-only match: returns params dict or nil
(define
  dr/route-params
  (fn
    (r req)
    (dr/match-segs
      (dr/segs (dream-route-path r))
      (dr/segs (dream-path req))
      {})))

;; method acceptance: exact, ANY, or HEAD served by a GET route
(define
  dr/method-accepts?
  (fn
    (route-method req-method)
    (or
      (= route-method "ANY")
      (= route-method req-method)
      (and (= req-method "HEAD") (= route-method "GET")))))

;; ── middleware pipeline (shared with middleware.sx) ────────────────
;; m1 @@ m2 @@ handler = (m1 (m2 handler)); first in list is outermost.
(define
  dr/apply-middlewares
  (fn (mws handler) (reduce (fn (h mw) (mw h)) handler (reverse mws))))

;; ── scope: prefix mount + middleware chain ─────────────────────────
;; Returns a flat list of routes; nested scopes flatten correctly.
(define
  dr/flatten-routes
  (fn
    (items)
    (reduce
      (fn
        (acc it)
        (if
          (dream-route? it)
          (concat acc (list it))
          (concat acc (dr/flatten-routes it))))
      (list)
      items)))

(define
  dream-scope
  (fn
    (prefix middlewares routes)
    (map
      (fn
        (r)
        (dream-route
          (dream-route-method r)
          (dr/join-path prefix (dream-route-path r))
          (dr/apply-middlewares middlewares (dream-route-handler r))))
      (dr/flatten-routes routes))))

;; ── dispatch ───────────────────────────────────────────────────────
;; allowed = methods of routes whose PATH matched (for 405 + Allow).
(define
  dr/dispatch
  (fn
    (routes req allowed)
    (if
      (empty? routes)
      (if
        (empty? allowed)
        (dream-not-found)
        (dream-method-not-allowed allowed))
      (let
        ((r (first routes)))
        (let
          ((params (dr/route-params r req)))
          (if
            (nil? params)
            (dr/dispatch (rest routes) req allowed)
            (if
              (dr/method-accepts? (dream-route-method r) (dream-method req))
              (dr/run-route r req params)
              (dr/dispatch
                (rest routes)
                req
                (concat allowed (list (dream-route-method r)))))))))))

;; run a matched route; blank the body for an auto-HEAD on a GET route
(define
  dr/run-route
  (fn
    (r req params)
    (let
      ((resp (dream-coerce-response ((dream-route-handler r) (dream-with-params req params)))))
      (if
        (and
          (= (dream-method req) "HEAD")
          (not (= (dream-route-method r) "HEAD")))
        (dream-response (dream-status resp) (dream-headers resp) "")
        resp))))

;; 405 response with an Allow header listing the path's methods
(define
  dream-method-not-allowed
  (fn
    (allowed)
    (dream-add-header
      (dream-response 405 {:content-type "text/plain; charset=utf-8"} "Method Not Allowed")
      "allow"
      (join ", " allowed))))

(define
  dream-router
  (fn
    (routes)
    (let
      ((flat (dr/flatten-routes routes)))
      (fn (req) (dr/dispatch flat req (list))))))
