;; lib/events/availability.sx — free/busy + conflict detection on Datalog.
;;
;; Availability is per-actor and is forward-chained Datalog over two EDB
;; relations:
;;
;;   (occurrence Key EventId Start End)   ; an expanded calendar occurrence
;;   (booking    Actor Key)               ; actor attends/holds that occurrence
;;
;; The derived relations are the whole policy:
;;
;;   busy(A,S,E)        — A is committed for [S,E) (a booked occurrence)
;;   conflict(A,O1,O2)  — A double-booked into two overlapping occurrences
;;   busy_in(A,QS,QE)   — A is busy somewhere inside query window [QS,QE)
;;
;; Intervals are half-open [Start,End) in epoch minutes (see calendar.sx), so
;; adjacent slots (E == next start) do NOT conflict. Conflict pairs are
;; canonical (O1 < O2 by key) so each overlap is reported once. The same `busy`
;; rule answers "is A free in [QS,QE)?" (busy_in is empty) and feeds "when is A
;; next free?" (ev-next-free probes candidate slots with the same rule).

;; A stable key for an occurrence dict {:id :start :end}.
(define ev-occ-key (fn (occ) (str (get occ :id) "@" (get occ :start))))

(define
  ev-occurrence-fact
  (fn
    (occ)
    (list
      (quote occurrence)
      (ev-occ-key occ)
      (get occ :id)
      (get occ :start)
      (get occ :end))))

(define ev-occurrence-facts (fn (occs) (map ev-occurrence-fact occs)))

(define ev-booking-fact (fn (actor key) (list (quote booking) actor key)))

(define ev-qwindow-fact (fn (qs qe) (list (quote qwindow) qs qe)))

;; Range restriction: each comparison's variables are bound by an earlier
;; positive literal (qwindow / busy precede the < tests). Conflict uses
;; (< O1 O2) on the keys so each overlapping pair is reported once.
(define
  ev-avail-rules
  (quote
    ((busy A S E <- (booking A O) (occurrence O _ S E))
      (conflict
        A
        O1
        O2
        <-
        (booking A O1)
        (booking A O2)
        (occurrence O1 _ S1 E1)
        (occurrence O2 _ S2 E2)
        (< O1 O2)
        (< S1 E2)
        (< S2 E1))
      (busy_in A QS QE <- (qwindow QS QE) (busy A S E) (< S QE) (< QS E)))))

;; Build a Datalog db from EDB facts under the availability ruleset.
(define ev-build-avail (fn (facts) (dl-program-data facts ev-avail-rules)))

;; Convenience: build a db from occurrence dicts + booking pairs.
;; bookings is a list of (actor key) pairs.
(define
  ev-avail-db
  (fn
    (occs bookings)
    (ev-build-avail
      (append
        (ev-occurrence-facts occs)
        (map
          (fn (b) (ev-booking-fact (first b) (first (rest b))))
          bookings)))))

;; Helper: insertion sort a list of (S E ...) lists ascending by S then E.
(define
  ev-list-before?
  (fn
    (a b)
    (cond
      ((< (first a) (first b)) true)
      ((> (first a) (first b)) false)
      (else (< (first (rest a)) (first (rest b)))))))

(define
  ev-list-insert
  (fn
    (x sorted)
    (cond
      ((empty? sorted) (list x))
      ((ev-list-before? x (first sorted)) (cons x sorted))
      (else (cons (first sorted) (ev-list-insert x (rest sorted)))))))

(define
  ev-sort-lists
  (fn (xs) (reduce (fn (acc x) (ev-list-insert x acc)) (list) xs)))

(define
  ev-dedup-sorted
  (fn
    (xs)
    (cond
      ((empty? xs) xs)
      ((empty? (rest xs)) xs)
      ((= (first xs) (first (rest xs))) (ev-dedup-sorted (rest xs)))
      (else (cons (first xs) (ev-dedup-sorted (rest xs)))))))

;; All busy intervals (list S E) for an actor, ascending by start.
(define
  ev-busy
  (fn
    (db actor)
    (let
      ((rows (dl-query db (list (quote busy) actor (quote S) (quote E)))))
      (ev-sort-lists (map (fn (b) (list (get b :S) (get b :E))) rows)))))

;; Distinct conflicting occurrence-key pairs for an actor (each pair once).
(define
  ev-conflicts
  (fn
    (db actor)
    (dl-query db (list (quote conflict) actor (quote O1) (quote O2)))))

(define
  ev-has-conflict?
  (fn (db actor) (> (len (ev-conflicts db actor)) 0)))

;; Is `actor` free across the whole window [qs,qe)? (no booked occurrence
;; overlaps it). Asserts a transient qwindow fact, queries, retracts.
(define
  ev-free?
  (fn
    (db actor qs qe)
    (do
      (dl-assert! db (ev-qwindow-fact qs qe))
      (let
        ((rows (dl-query db (list (quote busy_in) actor (quote QS) (quote QE)))))
        (begin (dl-retract! db (ev-qwindow-fact qs qe)) (empty? rows))))))

;; ---- next-free slot search ----
;; The earliest start s >= `after` such that [s, s+duration) is entirely free
;; for `actor` and ends at or before `horizon`, or nil if none. The earliest
;; such slot must begin either at `after` or immediately after some busy
;; interval ends (classic interval packing), so those are the only candidates
;; we probe — each probe reuses the busy_in rule via ev-free?.

(define
  ev-first-free
  (fn
    (db actor cands duration horizon)
    (cond
      ((empty? cands) nil)
      (else
        (let
          ((s (first cands)))
          (if
            (and
              (<= (+ s duration) horizon)
              (ev-free? db actor s (+ s duration)))
            s
            (ev-first-free db actor (rest cands) duration horizon)))))))

(define
  ev-next-free
  (fn
    (db actor after duration horizon)
    (let
      ((ends (filter (fn (e) (>= e after)) (map (fn (iv) (first (rest iv))) (ev-busy db actor)))))
      (ev-first-free
        db
        actor
        (ev-dedup-sorted (sort (cons after ends)))
        duration
        horizon))))
