;; lib/events/booking.sx — transactional, capacity-safe booking on persist.
;;
;; Each bookable occurrence has an append-only stream of booking events:
;;
;;   :booking  free booking — actor immediately holds a confirmed seat
;;   :hold     provisional hold — seat reserved while payment is pending
;;   :confirm  a held seat becomes confirmed (payment succeeded)
;;   :release  a held seat is abandoned (payment failed/expired) — seat freed
;;   :cancel   a held or confirmed seat is given up — seat freed
;;
;; The live state is the stream FOLDED in order into per-actor seat states
;; (:held / :confirmed); an actor in ANY state occupies a seat, so both held and
;; confirmed seats count toward capacity — a pending payment cannot be
;; oversold. A freed seat (release/cancel) reopens capacity.
;;
;; Capacity safety is the contract: two writers racing for the last seat must
;; NEVER both succeed. Seat-ACQUIRING writes (:booking, :hold) go through
;; persist's optimistic concurrency — `persist/append-expect` appends only if
;; the stream's last-seq still equals what the writer observed; else it returns
;; a conflict the writer retries. Seat-FREEING writes (:cancel, :release) and
;; the state transition (:confirm) never oversell, so they append directly.

(define ev-booking-stream (fn (occ-key) (str "booking:" occ-key)))

(define
  ev-bk-member?
  (fn
    (x xs)
    (cond
      ((empty? xs) false)
      ((= x (first xs)) true)
      (else (ev-bk-member? x (rest xs))))))

(define
  ev-bk-index
  (fn
    (xs x i)
    (cond
      ((empty? xs) -1)
      ((= (first xs) x) i)
      (else (ev-bk-index (rest xs) x (+ i 1))))))

(define ev-bk-append (fn (xs a) (append xs (list a))))
(define ev-bk-remove (fn (xs a) (filter (fn (x) (not (= x a))) xs)))

;; ---- per-actor state association list: ((actor state) ...) in join order ----

(define
  ev-state-has?
  (fn
    (states actor)
    (cond
      ((empty? states) false)
      ((= (first (first states)) actor) true)
      (else (ev-state-has? (rest states) actor)))))

(define
  ev-state-get
  (fn
    (states actor)
    (cond
      ((empty? states) :none)
      ((= (first (first states)) actor) (first (rest (first states))))
      (else (ev-state-get (rest states) actor)))))

(define
  ev-state-del
  (fn (states actor) (filter (fn (p) (not (= (first p) actor))) states)))

(define
  ev-state-set
  (fn
    (states actor st)
    (if
      (ev-state-has? states actor)
      (map (fn (p) (if (= (first p) actor) (list actor st) p)) states)
      (append states (list (list actor st))))))

;; Fold the booking stream into per-actor seat states (join order preserved).
(define
  ev-fold-states
  (fn
    (events)
    (reduce
      (fn
        (acc e)
        (let
          ((typ (persist/event-type e))
            (actor (get (persist/event-data e) :actor)))
          (cond
            ((= typ :booking) (ev-state-set acc actor :confirmed))
            ((= typ :hold) (ev-state-set acc actor :held))
            ((= typ :confirm)
              (if
                (ev-state-has? acc actor)
                (ev-state-set acc actor :confirmed)
                acc))
            ((= typ :cancel) (ev-state-del acc actor))
            ((= typ :release) (ev-state-del acc actor))
            (else acc))))
      (list)
      events)))

(define
  ev-states-of
  (fn
    (b occ-key)
    (ev-fold-states (persist/read b (ev-booking-stream occ-key)))))

;; Live roster (actors holding a seat — held or confirmed), oldest active first.
(define
  ev-booked-actors
  (fn (b occ-key) (map (fn (p) (first p)) (ev-states-of b occ-key))))

(define
  ev-actor-booked?
  (fn (b occ-key actor) (ev-bk-member? actor (ev-booked-actors b occ-key))))

;; Live seat count (folded roster size — both held and confirmed seats).
(define
  ev-booking-count
  (fn (b occ-key) (len (ev-booked-actors b occ-key))))

;; Seat state for an actor: :held / :confirmed / :none.
(define
  ev/seat-state
  (fn (b occ-key actor) (ev-state-get (ev-states-of b occ-key) actor)))

;; 1-based seat number for an actor on the roster (0 if not booked).
(define
  ev-seat-of
  (fn
    (actors actor)
    (let
      ((i (ev-bk-index actors actor 0)))
      (if (< i 0) 0 (+ i 1)))))

;; ---- seat-acquiring writes (capacity-guarded via append-expect) ----

;; One seat-acquiring attempt of `kind` (:booking or :hold) against an OBSERVED
;; snapshot (roster the writer saw + the last-seq). Returns :already / :full /
;; :conflict, or a success dict tagged with `ok-status`. :conflict means a
;; concurrent append landed since the snapshot — the caller must re-observe.
(define
  ev-acquire-with-observed
  (fn
    (b occ-key capacity actor observed-actors expected kind ok-status)
    (cond
      ((ev-bk-member? actor observed-actors) {:seat (ev-seat-of observed-actors actor) :actor actor :status :already})
      ((>= (len observed-actors) capacity) {:actor actor :capacity capacity :status :full})
      (else
        (let
          ((r (persist/append-expect b (ev-booking-stream occ-key) expected kind 0 {:actor actor})))
          (if (persist/conflict? r) {:actual (persist/conflict-actual r) :actor actor :status :conflict} {:seat (+ (len observed-actors) 1) :actor actor :status ok-status}))))))

(define
  ev-acquire!
  (fn
    (b occ-key capacity actor kind ok-status)
    (let
      ((res (ev-acquire-with-observed b occ-key capacity actor (ev-booked-actors b occ-key) (persist/last-seq b (ev-booking-stream occ-key)) kind ok-status)))
      (if
        (= (get res :status) :conflict)
        (ev-acquire! b occ-key capacity actor kind ok-status)
        res))))

;; Capacity-safe confirmed booking (retrying on conflict).
(define
  ev/book!
  (fn
    (b occ-key capacity actor)
    (ev-acquire! b occ-key capacity actor :booking :booked)))

;; Capacity-safe provisional hold (retrying on conflict). The seat is reserved
;; (counts toward capacity) until confirmed or released.
(define
  ev/hold!
  (fn
    (b occ-key capacity actor)
    (ev-acquire! b occ-key capacity actor :hold :held)))

;; Test seam: one attempt against a caller-supplied snapshot (book or hold).
(define
  ev/book-with-observed
  (fn
    (b occ-key capacity actor observed-actors expected)
    (ev-acquire-with-observed
      b
      occ-key
      capacity
      actor
      observed-actors
      expected
      :booking :booked)))

(define
  ev/hold-with-observed
  (fn
    (b occ-key capacity actor observed-actors expected)
    (ev-acquire-with-observed
      b
      occ-key
      capacity
      actor
      observed-actors
      expected
      :hold :held)))

;; ---- state transitions / seat-freeing writes (no oversell, append direct) ----

;; Confirm a held seat (payment succeeded). :confirmed on success,
;; :already-confirmed if it was confirmed, :not-held otherwise.
(define
  ev/confirm!
  (fn
    (b occ-key actor)
    (let
      ((st (ev/seat-state b occ-key actor)))
      (cond
        ((= st :held)
          (begin
            (persist/append
              b
              (ev-booking-stream occ-key)
              :confirm 0
              {:actor actor})
            {:actor actor :status :confirmed}))
        ((= st :confirmed) {:actor actor :status :already-confirmed})
        (else {:actor actor :status :not-held})))))

;; Release a held seat (payment failed/expired), freeing it. Only valid for a
;; held seat — confirmed bookings are given up via ev/cancel!.
(define
  ev/release!
  (fn
    (b occ-key actor)
    (let
      ((st (ev/seat-state b occ-key actor)))
      (if
        (= st :held)
        (begin
          (persist/append
            b
            (ev-booking-stream occ-key)
            :release 0
            {:actor actor})
          {:actor actor :status :released})
        {:actor actor :status :not-held}))))

;; Cancel a held or confirmed seat, freeing it. :cancelled or :not-booked.
(define
  ev/cancel!
  (fn
    (b occ-key actor)
    (if
      (ev-bk-member? actor (ev-booked-actors b occ-key))
      (begin
        (persist/append
          b
          (ev-booking-stream occ-key)
          :cancel 0
          {:actor actor})
        {:actor actor :status :cancelled})
      {:actor actor :status :not-booked})))

;; The roster as a plain list of actors (oldest active first).
(define ev/roster (fn (b occ-key) (ev-booked-actors b occ-key)))

;; Seats remaining for an occurrence of the given capacity.
(define
  ev/seats-left
  (fn
    (b occ-key capacity)
    (max 0 (- capacity (ev-booking-count b occ-key)))))

;; ---- waitlist ----
;; When an occurrence is full, actors join a FIFO waitlist (:waitlist /
;; :unwaitlist events on the same stream). Taking a seat (:booking / :hold)
;; removes an actor from the queue, so the waitlist fold is independent of the
;; seat fold. Cancelling/releasing a seat can auto-promote the head of the
;; queue (a :booking appended for them).

(define
  ev-fold-waiting
  (fn
    (events)
    (reduce
      (fn
        (acc e)
        (let
          ((typ (persist/event-type e))
            (actor (get (persist/event-data e) :actor)))
          (cond
            ((= typ :waitlist) (if (ev-bk-member? actor acc) acc (ev-bk-append acc actor)))
            ((= typ :unwaitlist) (ev-bk-remove acc actor))
            ((= typ :booking) (ev-bk-remove acc actor))
            ((= typ :hold) (ev-bk-remove acc actor))
            (else acc))))
      (list)
      events)))

;; The current waitlist queue (FIFO, oldest first).
(define
  ev/waitlist
  (fn (b occ-key) (ev-fold-waiting (persist/read b (ev-booking-stream occ-key)))))

;; 1-based queue position for an actor (0 if not waiting).
(define
  ev/waitlist-position
  (fn (b occ-key actor) (ev-seat-of (ev/waitlist b occ-key) actor)))

;; Book if a seat is free, else join the waitlist. Idempotent: already seated →
;; :already; already queued → :already-waiting.
(define
  ev/waitlist!
  (fn
    (b occ-key capacity actor)
    (let
      ((seats (ev-booked-actors b occ-key))
        (waiting (ev/waitlist b occ-key)))
      (cond
        ((ev-bk-member? actor seats)
          {:status :already :seat (ev-seat-of seats actor) :actor actor})
        ((ev-bk-member? actor waiting)
          {:status :already-waiting :position (ev-seat-of waiting actor) :actor actor})
        (else
          (let
            ((r (ev/book! b occ-key capacity actor)))
            (if
              (= (get r :status) :booked)
              r
              (begin
                (persist/append b (ev-booking-stream occ-key) :waitlist 0 {:actor actor})
                {:status :waitlisted
                  :position (+ (len waiting) 1)
                  :actor actor}))))))))

;; Leave the waitlist. :left or :not-waiting.
(define
  ev/leave-waitlist!
  (fn
    (b occ-key actor)
    (if
      (ev-bk-member? actor (ev/waitlist b occ-key))
      (begin
        (persist/append b (ev-booking-stream occ-key) :unwaitlist 0 {:actor actor})
        {:status :left :actor actor})
      {:status :not-waiting :actor actor})))

;; Cancel a seat and, if that frees capacity, auto-promote the head of the
;; waitlist (a confirmed booking). Returns the cancel result plus :promoted
;; (the actor promoted, or nil).
(define
  ev/cancel-promote!
  (fn
    (b occ-key capacity actor)
    (let
      ((c (ev/cancel! b occ-key actor)))
      (if
        (= (get c :status) :cancelled)
        (let
          ((waiting (ev/waitlist b occ-key))
            (seats (ev-booked-actors b occ-key)))
          (if
            (and (not (empty? waiting)) (< (len seats) capacity))
            (let
              ((promoted (first waiting)))
              (begin
                (persist/append b (ev-booking-stream occ-key) :booking 0 {:actor promoted})
                {:status :cancelled :actor actor :promoted promoted}))
            {:status :cancelled :actor actor :promoted nil}))
        c))))
