;; lib/acl/audit.sx — append-only decision log.
;;
;; Every decision routed through acl-audit-decide! is appended to an in-memory
;; log with a monotonic sequence number (no wall-clock — deterministic and
;; testable; a host can stamp time at the serializer boundary). The log is
;; append-only: there is no mutate or delete, only append, tail, clear,
;; snapshot/restore, and serialize-for-disk.

(define acl-audit-log (list))
(define acl-audit-seq 0)

;; Copy a list into a fresh, append!-able list. `map`/`rest`-derived lists are
;; NOT extensible by append! in this runtime (it silently no-ops), so the live
;; log must always be a list built with `list` + `append!`.
(define
  acl-audit-copy
  (fn
    (xs)
    (let
      ((fresh (list)))
      (do (for-each (fn (e) (append! fresh e)) xs) fresh))))

(define
  acl-audit-clear!
  (fn
    ()
    (do (set! acl-audit-log (list)) (set! acl-audit-seq 0) nil)))

;; Append a decision record. Returns the record.
(define
  acl-audit-record!
  (fn
    (subj act res allowed?)
    (let
      ((entry {:allowed? allowed? :act act :subj subj :res res :seq acl-audit-seq}))
      (do
        (set! acl-audit-seq (+ acl-audit-seq 1))
        (append! acl-audit-log entry)
        entry))))

;; Decide against db, log the outcome, and return the boolean. This is the
;; audited path; acl-permit? remains the pure, side-effect-free decision.
(define
  acl-audit-decide!
  (fn
    (db subj act res)
    (let
      ((allowed? (acl-permit? db subj act res)))
      (do (acl-audit-record! subj act res allowed?) allowed?))))

(define acl-audit-count (fn () (len acl-audit-log)))

;; Most recent n entries (in chronological order). n >= log size returns all.
(define
  acl-audit-tail
  (fn
    (n)
    (let
      ((total (len acl-audit-log)))
      (if
        (<= total n)
        acl-audit-log
        (acl-audit-drop acl-audit-log (- total n))))))

(define
  acl-audit-drop
  (fn
    (xs k)
    (if (<= k 0) xs (acl-audit-drop (rest xs) (- k 1)))))

;; Structured snapshot for save/restore — a {:seq :entries} value carrying a
;; copy of the log (so later appends don't mutate a held snapshot).
(define acl-audit-snapshot (fn () {:seq acl-audit-seq :entries (acl-audit-copy acl-audit-log)}))

;; Replace the live log from a snapshot. Restores both entries and the seq
;; counter so subsequent records continue numbering correctly. The log is
;; rebuilt as a fresh append!-able list (see acl-audit-copy).
(define
  acl-audit-restore!
  (fn
    (snap)
    (do
      (set! acl-audit-log (acl-audit-copy (get snap :entries)))
      (set! acl-audit-seq (get snap :seq))
      nil)))

;; Serialize the whole log to a disk-ready string: one record per line,
;; "seq\tsubj\tact\tres\tallowed?". A host writes this; structured reload is via
;; snapshot/restore.
(define
  acl-audit-serialize
  (fn
    ()
    (reduce
      (fn
        (acc e)
        (str
          acc
          (get e :seq)
          "\t"
          (get e :subj)
          "\t"
          (get e :act)
          "\t"
          (get e :res)
          "\t"
          (get e :allowed?)
          "\n"))
      ""
      acl-audit-log)))
