;; content-on-sx — op log + versioning over the persist event stream.
;;
;; The op log is the source of truth. Editing a document = appending the edit op
;; as a persist event to the document's stream. Any version of the document is a
;; replay of its op stream up to a sequence number; the materialised doc is a
;; cache, never primary state.
;;
;; Requires (loaded by the harness): block.sx, doc.sx, section.sx (doc-deep-find
;; + doc-tree-ids, for the tree-wide diff), plus persist (event/backend/log/kv/
;; api). The persist backend `b` is opened by the caller via (persist/open) and
;; injected — content knows nothing about which backend.

(define content/-stream (fn (doc-id) (str "content:" doc-id)))

;; ── commit: append an edit op as an event. `at` is a caller-supplied logical
;; timestamp (Date.now is unavailable in-kernel). Returns the stored event. ──
(define
  content/commit!
  (fn
    (b doc-id op at)
    (persist/append b (content/-stream doc-id) (get op :op) at op)))

(define
  content/commit-all!
  (fn
    (b doc-id ops at)
    (if
      (= (len ops) 0)
      nil
      (begin
        (content/commit! b doc-id (first ops) at)
        (content/commit-all! b doc-id (rest ops) at)))))

;; ── read the raw log / op stream ──
(define
  content/log
  (fn (b doc-id) (persist/read b (content/-stream doc-id))))

(define
  content/ops
  (fn
    (b doc-id)
    (map (fn (ev) (persist/event-data ev)) (content/log b doc-id))))

;; logical version count (highest seq assigned, survives compaction)
(define
  content/version-count
  (fn (b doc-id) (persist/last-seq b (content/-stream doc-id))))

;; ── replay ──
;; head — materialise the latest document by folding all ops.
(define
  content/head
  (fn (b doc-id) (doc-apply-all (doc-empty doc-id) (content/ops b doc-id))))

;; at — materialise the document as of sequence `seq` (a version).
(define
  content/at
  (fn
    (b doc-id seq)
    (let
      ((evs (filter (fn (ev) (<= (persist/event-seq ev) seq)) (content/log b doc-id))))
      (doc-apply-all
        (doc-empty doc-id)
        (map (fn (ev) (persist/event-data ev)) evs)))))

;; ── history: per-version metadata, oldest-first ──
(define
  content/history
  (fn (b doc-id) (map (fn (ev) {:type (persist/event-type ev) :at (persist/event-at ev) :seq (persist/event-seq ev)}) (content/log b doc-id))))

;; ── diff between two materialised document versions ──
;; Tree-wide: ids are enumerated across the whole block tree (descending into
;; sections), so nested-block adds/removes/changes are detected, not just
;; top-level ones. Returns {:added :removed :changed} (lists of ids):
;;   :added   — ids present (anywhere) in `new` but not in `old`
;;   :removed — ids present (anywhere) in `old` but not in `new`
;;   :changed — content blocks present in both whose block value differs
;; Section containers never appear in :changed (they hold no own content — a
;; child change surfaces as that child's own entry); a whole section appearing
;; or disappearing shows up in :added / :removed by its id.
(define content/-all-ids (fn (doc) (doc-tree-ids doc)))

(define content/-missing? (fn (doc id) (= (doc-deep-find doc id) nil)))

(define
  content/-changed
  (fn
    (old new)
    (filter
      (fn
        (id)
        (let
          ((bo (doc-deep-find old id)) (bn (doc-deep-find new id)))
          (cond
            ((= bo nil) false)
            ((= bn nil) false)
            ((= (blk-type bo) "section") false)
            ((= bo bn) false)
            (else true))))
      (content/-all-ids old))))

(define content/diff (fn (old new) {:changed (content/-changed old new) :removed (filter (fn (id) (content/-missing? new id)) (content/-all-ids old)) :added (filter (fn (id) (content/-missing? old id)) (content/-all-ids new))}))

;; convenience: diff two persisted versions by seq.
(define
  content/diff-versions
  (fn
    (b doc-id seq-a seq-b)
    (content/diff (content/at b doc-id seq-a) (content/at b doc-id seq-b))))
