;; content-on-sx — document integrity validation.
;;
;; Guards imports, edits and federated input: walks the whole block TREE (into
;; nested sections) checking each block's id and required fields/types, plus
;; tree-wide duplicate ids. Returns issue dicts {:id :kind :detail}; empty = ok.
;; Tree detection is inline (class + st-iv-get) so this file needs no section.sx.
;; Dispatch on block type is a validation-boundary concern, not core behaviour.
;;
;; Requires (loaded by harness): block.sx, doc.sx.

(define ct-issue (fn (id kind detail) {:id id :detail detail :kind kind}))

(define
  ct-flatmap
  (fn
    (f xs)
    (if
      (= (len xs) 0)
      (list)
      (append (f (first xs)) (ct-flatmap f (rest xs))))))

(define ct-count-in (fn (x xs) (len (filter (fn (y) (= y x)) xs))))

;; dedup, order-preserving (keep first occurrence)
(define
  ct-uniq-loop
  (fn
    (xs seen)
    (if
      (= (len xs) 0)
      (reverse seen)
      (if
        (> (ct-count-in (first xs) seen) 0)
        (ct-uniq-loop (rest xs) seen)
        (ct-uniq-loop (rest xs) (cons (first xs) seen))))))

(define ct-uniq (fn (xs) (ct-uniq-loop xs (list))))

;; ── tree flatten (descends into CtSection children; guards malformed children) ──
(define
  ct-section-block?
  (fn (b) (and (st-instance? b) (= (get b :class) "CtSection"))))
(define
  ct-tree-blocks
  (fn
    (blocks)
    (if
      (= (len blocks) 0)
      (list)
      (let
        ((b (first blocks)))
        (append
          (cons
            b
            (if
              (ct-section-block? b)
              (let
                ((ch (st-iv-get b "children")))
                (if (list? ch) (ct-tree-blocks ch) (list)))
              (list)))
          (ct-tree-blocks (rest blocks)))))))

;; ── id checks ──
(define
  content/-id-issues
  (fn
    (b)
    (let
      ((id (blk-id b)))
      (if
        (and (string? id) (> (len id) 0))
        (list)
        (list (ct-issue id "id" "block id must be a non-empty string"))))))

(define
  ct-field-issue
  (fn (id ok? what) (if ok? (list) (list (ct-issue id "field" what)))))

;; ── per-type field checks ──
(define
  content/-field-issues
  (fn
    (b)
    (let
      ((t (blk-type b)) (id (blk-id b)))
      (cond
        ((= t "heading")
          (append
            (ct-field-issue
              id
              (number? (blk-get b "level"))
              "heading level must be a number")
            (ct-field-issue
              id
              (string? (blk-get b "text"))
              "heading text must be a string")))
        ((= t "text")
          (ct-field-issue
            id
            (string? (blk-get b "text"))
            "text must be a string"))
        ((= t "code")
          (append
            (ct-field-issue
              id
              (string? (blk-get b "language"))
              "code language must be a string")
            (ct-field-issue
              id
              (string? (blk-get b "text"))
              "code text must be a string")))
        ((= t "quote")
          (ct-field-issue
            id
            (string? (blk-get b "text"))
            "quote text must be a string"))
        ((= t "image")
          (append
            (ct-field-issue
              id
              (string? (blk-get b "src"))
              "image src must be a string")
            (ct-field-issue
              id
              (string? (blk-get b "alt"))
              "image alt must be a string")))
        ((= t "embed")
          (append
            (ct-field-issue
              id
              (string? (blk-get b "url"))
              "embed url must be a string")
            (ct-field-issue
              id
              (string? (blk-get b "provider"))
              "embed provider must be a string")))
        ((= t "divider") (list))
        ((= t "list")
          (append
            (ct-field-issue
              id
              (boolean? (blk-get b "ordered"))
              "list ordered must be a boolean")
            (ct-field-issue
              id
              (list? (blk-get b "items"))
              "list items must be a list")))
        ((= t "section")
          (ct-field-issue
            id
            (list? (blk-get b "children"))
            "section children must be a list"))
        ((= t "table")
          (append
            (ct-field-issue
              id
              (list? (blk-get b "headers"))
              "table headers must be a list")
            (ct-field-issue
              id
              (list? (blk-get b "rows"))
              "table rows must be a list")))
        ((= t "callout")
          (append
            (ct-field-issue
              id
              (string? (blk-get b "kind"))
              "callout kind must be a string")
            (ct-field-issue
              id
              (string? (blk-get b "text"))
              "callout text must be a string")))
        ((= t "media")
          (append
            (ct-field-issue
              id
              (if
                (= (blk-get b "kind") "video")
                true
                (= (blk-get b "kind") "audio"))
              "media kind must be video or audio")
            (ct-field-issue
              id
              (string? (blk-get b "src"))
              "media src must be a string")))
        (else (list (ct-issue id "type" (str "unknown block type: " t))))))))

(define
  content/-block-issues
  (fn (b) (append (content/-id-issues b) (content/-field-issues b))))

;; ── duplicate ids across the whole tree ──
(define
  content/-dup-issues
  (fn
    (ids)
    (map
      (fn (id) (ct-issue id "duplicate" (str "duplicate block id: " id)))
      (ct-uniq (filter (fn (id) (> (ct-count-in id ids) 1)) ids)))))

;; ── public ──
(define
  content/validate
  (fn
    (doc)
    (let
      ((all (ct-tree-blocks (doc-blocks doc))))
      (append
        (content/-dup-issues (map (fn (b) (blk-id b)) all))
        (ct-flatmap content/-block-issues all)))))

(define
  content/valid?
  (fn (doc) (= (len (content/validate doc)) 0)))

(define
  content/issue-kinds
  (fn (doc) (map (fn (i) (get i :kind)) (content/validate doc))))
