;; lib/host/session.sx — durable, signed sessions for the host.
;; Backs Dream's session middleware ops (session/create|exists|get|set|clear)
;; with the SAME durable persist KV the blog uses, so a login survives restarts.
;; The session cookie carries only a signed sid (dream-sessions-signed): the sid
;; itself is a persisted monotonic counter ("s1", "s2", …) — cheap and ordered —
;; and the HMAC signature (dr/sess-hash, keyed by host/session-secret) makes a
;; guessed or forged cookie unusable. http-listen serialises handler calls under a
;; mutex, so the counter increment is race-free.
;;
;; Depends on lib/dream/session.sx (dream-sessions-signed + cookie helpers) and
;; lib/persist/* (the KV backend). Wired into host/make-app via host/sessions.

;; ── store (durable persist KV, injectable; mirrors host/blog-store) ──
(define host/session-store (persist/open))
(define host/session-use-store! (fn (b) (set! host/session-store b)))

;; ── signing secret (override from $SX_SESSION_SECRET in serve.sh) ────
(define host/session-secret "rose-ash-host-dev-secret-change-me")
(define host/session-set-secret! (fn (s) (set! host/session-secret s)))

;; ── keys ────────────────────────────────────────────────────────────
(define host/-sess-key (fn (sid) (str "session:" sid)))
(define host/-sess-epoch-key "session:-epoch")

;; sid generation: a per-BOOT epoch (one durable write at startup) + an in-memory
;; counter. The epoch keeps sids unique across restarts WITHOUT a write per
;; request, so anonymous traffic costs no disk. host/session-init! bumps the epoch
;; on boot (serve.sh); without it (e.g. tests) epoch 0 is fine within one process.
(define host/session-epoch 0)
(define host/session-ctr 0)
(define host/session-init!
  (fn ()
    (let ((e (+ 1 (or (persist/backend-kv-get host/session-store host/-sess-epoch-key) 0))))
      (begin
        (persist/backend-kv-put host/session-store host/-sess-epoch-key e)
        (set! host/session-epoch e)
        (set! host/session-ctr 0)))))
(define host/-sess-next-sid
  (fn ()
    (begin
      (set! host/session-ctr (+ host/session-ctr 1))
      (str "s" host/session-epoch "-" host/session-ctr))))

;; ── backend io fn: dispatch session/* ops onto the persist KV ───────
;; LAZY: session/create mints a sid but writes NO row, so an anonymous request
;; (which never sets a field) leaves no durable trace — the store isn't spammed by
;; crawlers. The row appears on the first session/set (i.e. login), so a logged-in
;; session persists and survives a restart; session/exists is "has a written row".
(define host/session-backend
  (fn (op)
    (let ((kind (get op :op)))
      (cond
        ((= kind "session/create") (host/-sess-next-sid))
        ((= kind "session/exists")
          (persist/backend-kv-has? host/session-store (host/-sess-key (get op :sid))))
        ((= kind "session/get")
          (get
            (or (persist/backend-kv-get host/session-store (host/-sess-key (get op :sid))) {})
            (get op :key)))
        ((= kind "session/set")
          (let ((sid (get op :sid)))
            (persist/backend-kv-put host/session-store (host/-sess-key sid)
              (assoc
                (or (persist/backend-kv-get host/session-store (host/-sess-key sid)) {})
                (get op :key)
                (get op :val)))))
        ((= kind "session/load")
          (or (persist/backend-kv-get host/session-store (host/-sess-key (get op :sid))) {}))
        ((= kind "session/clear")
          (persist/backend-kv-delete host/session-store (host/-sess-key (get op :sid))))
        (else nil)))))

;; ── middleware for the host pipeline: signed cookie + durable backend ─
(define host/sessions
  (fn () (dream-sessions-signed host/session-backend host/session-secret)))

;; ── handler-facing helpers ──────────────────────────────────────────
;; The logged-in principal (or nil), and login/logout writing the session field.
(define host/current-principal (fn (req) (dream-session-field req :principal)))
(define host/login! (fn (req principal) (dream-set-session-field req :principal principal)))
(define host/logout! (fn (req) (dream-invalidate-session req)))
