;;; recently-updated.el --- Generate recently-updated.org -*- lexical-binding: t; -*-

;;; Commentary:
;; Generates home/recently-updated.org listing the N most recently touched
;; Org files in the site tree.
;;
;; Sort order: #+DATE if parseable, otherwise file modification time.
;; This means intentionally dated pages float correctly, while undated
;; pages fall back to when they were last touched on disk.
;;
;; NOTE: Do NOT define `site-root' here.  build-site.el owns that definition.

;;; Code:

(require 'cl-lib)
(require 'org)

;; ── Omit predicate ────────────────────────────────────────────────────────────

(defconst z/recent-omit-names
  '("sitemap.org"
    "categories.org"
    "recently-updated.org"
    "wip.org")
  "Org files that are always excluded from the recently-updated list.")

(defun z/recent-omit-org-p (rel-org)
  "Return non-nil if REL-ORG (relative to site root) should be excluded."
  (let ((name (file-name-nondirectory rel-org)))
    (or
     ;; Static asset directories
     (string-prefix-p "assets/" rel-org)
     (string-match-p  "/assets/" rel-org)
     ;; Tag pages
     (string-prefix-p "tags/" rel-org)
     (string-match-p  "/tags/" rel-org)
     ;; Known generated files
     (member name z/recent-omit-names)
     ;; Any *-list.org sitemaps
     (string-match-p "-list\\.org\\'" name))))

;; ── Metadata reader ───────────────────────────────────────────────────────────

(defun z/read-org-meta (file)
  "Return (TITLE . TIME) for Org FILE.

TITLE: from #+TITLE, or file-name-base when absent.
TIME : from #+DATE (parsed as an Org time string) when present and
       parseable; otherwise falls back to the file's modification time.
       Using #+DATE ensures intentionally-dated content sorts by its
       canonical publication date rather than an accidental disk mtime."
  (with-temp-buffer
    (insert-file-contents file)
    (org-mode)
    (let* ((props      (org-collect-keywords '("TITLE" "DATE")))
           (title      (or (cadr (assoc "TITLE" props))
                           (file-name-base file)))
           (date-str   (cadr (assoc "DATE" props)))
           (time       (or (and date-str
                                (ignore-errors
                                  (org-time-string-to-time date-str)))
                           (file-attribute-modification-time
                            (file-attributes file)))))
      (cons title time))))

;; ── Generator ────────────────────────────────────────────────────────────────

(defun z/generate-recently-updated-org (&optional n)
  "Write home/recently-updated.org listing the top N recently updated files.
N defaults to 30.  Returns the number of entries written."
  (let* ((count     (or n 30))
         (org-root  (file-name-as-directory z/site-root))
         (all-files (directory-files-recursively org-root "\\.org\\'"))
         (items     '()))
    ;; Collect (REL TITLE TIME)
    (dolist (full all-files)
      (let ((rel (file-relative-name full org-root)))
        (unless (z/recent-omit-org-p rel)
          (pcase-let ((`(,title . ,time) (z/read-org-meta full)))
            (push (list rel title time) items)))))
    ;; Sort newest first
    (setq items (sort items (lambda (a b) (time-less-p (nth 2 b) (nth 2 a)))))
    ;; Trim to N
    (setq items (cl-subseq items 0 (min count (length items))))
    ;; Write Org
    (with-temp-file (site-path "recently-updated.org")
      (insert "#+TITLE: Recently Updated\n"
              "#+OPTIONS: toc:nil num:nil\n\n"
              (format "* Recently Updated (top %d files)\n" (length items)))
      (dolist (it items)
        (let* ((rel     (nth 0 it))
               (title   (nth 1 it))
               (time    (nth 2 it))
               (datestr (format-time-string "%Y-%m-%d %H:%M" time)))
          (insert (format "- [[file:%s][%s]] @@html:<span class=\"post-date\">%s</span>@@\n"
                          rel title datestr)))))
    (length items)))

(provide 'recently-updated)

;;; recently-updated.el ends here
