;;; -*- lexical-binding: t -*-
(defun my/update-org-agenda ()
  (interactive)
  (let ((project-files
         (when (file-directory-p (concat org-directory "/projects"))
           (thread-last "/projects"
                        (concat org-directory)
                        (directory-files)
                        (mapcar (lambda (f)
                                  (concat
                                   org-directory "/projects/" f)))
                        (seq-filter (lambda (f)
                                      (not (file-directory-p f))))))))
    (setq org-agenda-files
          (seq-filter #'file-exists-p
                      (append
                       project-files
                       (mapcar (lambda (f)
                                 (concat org-directory "/" f))
                               '("inbox.org"
                                 "misc/habit.org"
                                 "contacts.org")))))
    (setq org-refile-targets
          `(,@(mapcar
               (lambda (f) `(,f . (:tag . "refile")))
               project-files)
            ,@(mapcar
               (lambda (f) `(,f . (:regexp . "Tasks")))
               project-files)))
    (when (file-exists-p (concat org-directory "/scripts/refile.el"))
      (load-file (concat org-directory "/scripts/refile.el"))
      (run-hooks 'my/org-refile-hooks))))

(setq org-roam-directory (concat org-directory "/roam"))
(with-eval-after-load 'org
  (require 'seq)
  (my/update-org-agenda))

(setq org-refile-use-outline-path 'file)
(setq org-outline-path-complete-in-steps nil)

(setq org-extend-today-until 4)

(defun my/generate-inbox-note-name ()
  (format
   "%s/inbox-notes/%s%s.org"
   org-directory
   (format-time-string "%Y%m%d%H%M%S")
   (let ((note-name (read-string "Note name: ")))
     (if (not (string-empty-p note-name))
         (string-replace " " "-" (concat "-" (downcase note-name)))
       ""))))

(setq org-capture-templates
      `(("i" "Inbox" entry  (file "inbox.org")
         ,(concat "* TODO %?\n"
                  "/Entered on/ %U"))
        ("e" "email" entry (file "inbox.org")
         ,(concat "* TODO %:from %:subject \n"
                  "/Entered on/ %U\n"
                  "/Received on/ %:date-timestamp-inactive\n"
                  "%a\n"))
        ("f" "elfeed" entry (file "inbox.org")
         ,(concat "* TODO %:elfeed-entry-title\n"
                  "/Entered on/ %U\n"
                  "%a\n"))
        ("n" "note" plain (file my/generate-inbox-note-name)
         ,(concat "#+TODO: PROCESSED(p)\n"
                  "\n"
                  "* %?\n"
                  "/Entered on/ %U"))))

(use-package org-clock-agg
  :straight (:host github :repo "SqrtMinusOne/org-clock-agg")
  :commands (org-clock-agg)
  :init
  (with-eval-after-load 'org
    (my-leader-def "ol" #'org-clock-agg))
  :config
  (setq org-clock-agg-node-format
    "%-%(+ title-width)t %20c %8z %s/%S")
  (setq org-clock-agg-node-title-width-delta 47)
  (push
   (cons "Agenda+Archive"
         (append
          (org-agenda-files)
          (thread-last "/projects/archive"
                       (concat org-directory)
                       (directory-files)
                       (mapcar (lambda (f)
                                 (concat
                                  org-directory "/projects/archive/" f)))
                       (seq-filter (lambda (f)
                                     (not (file-directory-p f)))))))
   org-clock-agg-files-preset))

(with-eval-after-load 'org
  (setq org-clock-persist 'clock)
  (org-clock-persistence-insinuate))

(with-eval-after-load 'org
  (add-to-list
   'org-global-properties
   '("Effort_ALL" . "0 0:05 0:10 0:15 0:30 0:45 1:00 1:30 2:00 4:00 8:00")))

(setq org-log-done 'time)

(defun my/org-clock-in--fix-mode-line ()
  (when (memq 'org-mode-line-string global-mode-string)
    (let (new-global-mode-string
          appended
          (is-first t))
      (dolist (item global-mode-string)
        (cond
         ((or (equal item '(:eval (exwm-modeline-segment)))
              (equal item '(:eval (persp-mode-line))))
          (unless appended
            (when is-first
              (push "" new-global-mode-string))
            (push 'org-mode-line-string new-global-mode-string)
            (setq appended t))
          (push item new-global-mode-string))
         ((equal item 'org-mode-line-string))
         (t
          (push item new-global-mode-string)))
        (setq is-first nil))
      (unless appended
        (push 'org-mode-line-string new-global-mode-string))
      (setq global-mode-string (nreverse new-global-mode-string)))))

(add-hook 'org-clock-in-hook #'my/org-clock-in--fix-mode-line)

(defun my/org-clock-in-prompt-time (&optional select)
  (interactive "P")
  (org-clock-in
   select
   (encode-time
    (org-parse-time-string
     (org-read-date t)))))

(with-eval-after-load 'org
  (my-leader-def
    :keymaps 'org-mode-map
    :infix "SPC"
    "I" #'my/org-clock-in-prompt-time))

(defun my/org-clock-get-total-minutes-at-point ()
  "Get total clocked time for heading at point."
  (let* ((element (org-element-at-point-no-context))
         (s (buffer-substring-no-properties
             (org-element-property :begin element)
             (org-element-property :end element))))
    (with-temp-buffer
      (insert s)
      (org-clock-sum)
      org-clock-file-total-minutes)))

(defconst my/org-clock-total-prop :CLOCK_TOTAL)

(defun my/org-clock-set-total-clocked ()
  "Set total clocked time for heading at point."
  (interactive)
  (save-excursion
    (org-back-to-heading t)
    (org-set-property
     (substring
      (symbol-name my/org-clock-total-prop)
      1)
     (org-duration-from-minutes
      (my/org-clock-get-total-minutes-at-point)))))

(add-hook 'org-clock-in-hook #'my/org-clock-set-total-clocked)
(add-hook 'org-clock-out-hook #'my/org-clock-set-total-clocked)
(add-hook 'org-clock-cancel-hook #'my/org-clock-set-total-clocked)

(defun my/org-clock-recent ()
  (interactive)
  (let* ((entries (org-ql-query
                    :select #'element-with-markers
                    :from (org-agenda-files)
                    :where '(clocked :from -1)))
         (entries-data (mapcar (lambda (e)
                                 (cons (org-element-property :raw-value e) e))
                               entries)))
    (unless entries
      (user-error "No recently clocked entries!"))
    entries-data
    (let* ((entry (alist-get (completing-read "Entry: " entries-data)
                             entries-data nil nil #'equal))
           (marker (org-element-property :org-marker entry)))
      (pop-to-buffer-same-window (marker-buffer marker))
      (goto-char marker))))

(with-eval-after-load 'org
  (my-leader-def
    :keymaps 'org-mode-map
    :infix "SPC"
    "C" #'my/org-clock-recent))

(defun my/org-fix-task-kind ()
  (interactive)
  (let ((entries (org-ql-query
                   :select #'element-with-markers
                   :from (current-buffer)
                   :where '(and (olp "Tasks")
                                (not (property "TASK_KIND"))
                                (clocked)))))
    (org-fold-show-all)
    (dolist (entry entries)
      (let ((marker (org-element-property :org-marker entry)))
        (org-with-point-at marker
          (let ((value (org-read-property-value "TASK_KIND")))
            (org-set-property "TASK_KIND" value)))))))

(use-package org-super-agenda
  :straight t
  :after (org)
  :config
  ;; Alphapapa doesn't like evil
  (general-define-key
   :keymaps '(org-super-agenda-header-map)
   "h" nil
   "j" nil
   "k" nil
   "l" nil)
  (org-super-agenda--def-auto-group outline-path-file "their outline paths & files"
    :key-form
    (org-super-agenda--when-with-marker-buffer (org-super-agenda--get-marker item)
      ;; org-ql depends on f and s anyway
      (s-join "/" (cons
                   (f-filename (buffer-file-name))
                   (org-get-outline-path))))))

(defun my/org-super-agenda--make-agenda-header-around (fun name)
  (remove-text-properties 0 (length name) '(line-prefix nil) name)
  (remove-text-properties 0 (length name) '(wrap-prefix nil) name)
  (funcall fun (substring-no-properties name)))

(with-eval-after-load 'org-super-agenda
  (advice-add 'org-super-agenda--make-agenda-header :around #'my/org-super-agenda--make-agenda-header-around))

(use-package org-ql
  :after (org)
  :if (not my/remote-server)
  :straight t
  :config
  (setq org-ql-ask-unsafe-queries nil)
  :init
  ;; See https://github.com/alphapapa/org-ql/pull/237
  (setq org-ql-regexp-part-ts-time
        (rx " " (repeat 1 2 digit) ":" (repeat 2 digit)
            (optional "-" (repeat 1 2 digit) ":" (repeat 2 digit))))
  (my-leader-def
    :infix "o"
    "v" #'org-ql-view
    "q" #'org-ql-search))

(cl-defun my/org-ql-view-recent-items
    (&key num-days (type 'ts)
          (files (org-agenda-files))
          (groups '((:auto-outline-path-file t)
                    (:auto-todo t))))
  "Show items in FILES from last NUM-DAYS days with timestamps of TYPE.
TYPE may be `ts', `ts-active', `ts-inactive', `clocked', or
`closed'."
  (interactive (list :num-days (read-number "Days: ")
                     :type (->> '(ts ts-active ts-inactive clocked closed)
                                (completing-read "Timestamp type: ")
                                intern)))
  ;; It doesn't make much sense to use other date-based selectors to
  ;; look into the past, so to prevent confusion, we won't allow them.
  (-let* ((query (pcase-exhaustive type
                   ((or 'ts 'ts-active 'ts-inactive)
                    `(,type :from ,(- num-days) :to 0))
                   ((or 'clocked 'closed)
                    `(,type :from ,(- num-days) :to 0)))))
    (org-ql-search files query
      :title "Recent items"
      :sort '(todo priority date)
      :super-groups groups)))

(defun my/org-ql-all-todo ()
  (interactive)
  ;; The hack I borrowed from notmuch to make " " a separator
  (let* ((crm-separator " ")
         (crm-local-completion-map
          (let ((map (make-sparse-keymap)))
            (set-keymap-parent map crm-local-completion-map)
            (define-key map " " 'self-insert-command)
            map))
         (vertico-sort-function nil)
         (categories (completing-read-multiple
                      "Categories: "
                      '("TEACH" "EDU" "JOB" "LIFE" "COMP"))))
    (org-ql-search (org-agenda-files)
      `(and (todo)
            ,@(unless (seq-empty-p categories)
                `((category ,@categories))))
      :sort '(priority todo deadline)
      :super-groups '((:auto-outline-path-file t)))))

(defun my/org-ql-clocked-today ()
  (interactive)
  (let ((today (format-time-string
                "%Y-%m-%d"
                (days-to-time
                 (- (org-today) (time-to-days 0))))))
    (org-ql-search (org-agenda-files) `(clocked :from ,today)
      :title "Clocked today"
      :sort '(todo priority date)
      :super-groups '((:auto-outline-path-file t)
                      (:auto-todo t)))))

(defun my/org-ql-closed-today ()
  (interactive)
  (let ((today (format-time-string
                "%Y-%m-%d"
                (days-to-time
                 (- (org-today) (time-to-days 0))))))
    (org-ql-search (org-agenda-files) `(closed :from ,today)
      :title "Closed today"
      :sort '(todo priority date)
      :super-groups '((:auto-outline-path-file t)
                      (:auto-todo t)))))

(setq org-ql-views
      (list
       (cons "Overview: All TODO" #'my/org-ql-all-todo)
       (cons "Review: Stale tasks"
             (list :buffers-files #'org-agenda-files
                   :query '(and (todo)
                                (not (tags "nots"))
                                (not (ts :from -14)))
                   :title "Review: Stale tasks"
                   :sort '(todo priority date)
                   :super-groups '((:auto-outline-path-file t))))
       (cons "Review: Unclocked tasks"
             (list :buffers-files #'org-agenda-files
                   :query '(and (done)
                                (ts :from -14)
                                (not (clocked))
                                (not (tags "nots")))
                   :title "Review: Unclocked tasks"
                   :sort '(todo priority date)
                   :super-groups '((:auto-outline-path-file t))))
       (cons "Review: Recently timestamped" #'my/org-ql-view-recent-items)
       (cons "Review: Clocked today" #'my/org-ql-clocked-today)
       (cons "Review: Closed today" #'my/org-ql-closed-today)
       (cons "Fix: tasks without TASK_KIND"
             (lambda ()
               (interactive)
               (org-ql-search (current-buffer)
                 '(and (olp "Tasks")
                       (not (property "TASK_KIND"))
                       (clocked))
                 :super-groups '((:auto-outline-path-file t)))))))

(defun my/org-ql-view--format-element-override (element)
  "Format ELEMENT for `org-ql-view'.

Check `org-ql-view--format-element' for the original implementation
and lots of comments which are too long for my Emacs config."
  (if (not element)
      ""
    (setf element (org-ql-view--resolve-element-properties element))
    (let* ((properties (cadr element))
           (properties (cl-loop for (key val) on properties by #'cddr
                                for symbol = (intern (cl-subseq (symbol-name key) 1))
                                unless (member symbol '(parent))
                                append (list symbol val)))
           (title (--> (org-ql-view--add-faces element)
                       (org-element-property :raw-value it)
                       (org-link-display-format it)))
           (todo-keyword (-some--> (org-element-property :todo-keyword element)
                           (org-ql-view--add-todo-face it)))
           (tag-list (if org-use-tag-inheritance
                         (if-let ((marker (or (org-element-property :org-hd-marker element)
                                              (org-element-property :org-marker element))))
                             (with-current-buffer (marker-buffer marker)
                               (org-with-wide-buffer
                                (goto-char marker)
                                (cl-loop for type in (org-ql--tags-at marker)
                                         unless (or (eq 'org-ql-nil type)
                                                    (not type))
                                         append type)))
                           (display-warning 'org-ql (format "No marker found for item: %s" title))
                           (org-element-property :tags element))
                       (org-element-property :tags element)))
           (tag-string (when tag-list
                         (--> tag-list
                              (s-join ":" it)
                              (s-wrap it ":")
                              (org-add-props it nil 'face 'org-tag))))
           ;;  (category (org-element-property :category element))
           (priority-string (-some->> (org-element-property :priority element)
                              (char-to-string)
                              (format "[#%s]")
                              (org-ql-view--add-priority-face)))
           (clock-string (let ((effort (org-element-property :EFFORT element))
                               (clocked (org-element-property my/org-clock-total-prop element)))
                           (cond
                            ((and clocked effort) (format "[%s/%s]" clocked effort))
                            ((and clocked (not effort) (format "[%s]" clocked)))
                            ((and (not clocked) effort) (format "[EST: %s]" effort)))))
           (habit-property (org-with-point-at (or (org-element-property :org-hd-marker element)
                                                  (org-element-property :org-marker element))
                             (when (org-is-habit-p)
                               (org-habit-parse-todo))))
           (due-string (pcase (org-element-property :relative-due-date element)
                         ('nil "")
                         (string (format " %s " (org-add-props string nil 'face 'org-ql-view-due-date)))))
           (string (s-join " " (-non-nil (list todo-keyword priority-string title due-string clock-string tag-string)))))
      (remove-list-of-text-properties 0 (length string) '(line-prefix) string)
      (--> string
           (concat "  " it)
           (org-add-props it properties
             'org-agenda-type 'search
             'todo-state todo-keyword
             'tags tag-list
             'org-habit-p habit-property)))))

(with-eval-after-load 'org-ql
  (advice-add #'org-ql-view--format-element :override #'my/org-ql-view--format-element-override))

(use-package org-habit-stats
  :straight (:host github :repo "ml729/org-habit-stats")
  :after (org)
  :config
  (general-define-key
   :keymaps '(org-habit-stats-mode-map)
   :states '(normal emacs)
   "q" #'org-habit-stats-exit
   "<" #'org-habit-stats-calendar-scroll-left
   ">" #'org-habit-stats-calendar-scroll-right
   "[" #'org-habit-stats-scroll-graph-left
   "]" #'org-habit-stats-scroll-graph-right
   "{" #'org-habit-stats-scroll-graph-left-big
   "}" #'org-habit-stats-scroll-graph-right-big
   "." #'org-habit-stats-view-next-habit
   "," #'org-habit-stats-view-previous-habit)
   (add-hook 'org-after-todo-state-change-hook 'org-habit-stats-update-properties))

(defun my/org-match-at-point-p (match)
  "Return non-nil if headline at point matches MATCH.
Here MATCH is a match string of the same format used by
`org-tags-view'."
  (funcall (cdr (org-make-tags-matcher match))
           (org-get-todo-state)
           (org-get-tags-at)
           (org-reduced-level (org-current-level))))

(defun my/org-agenda-skip-without-match (match)
  "Skip current headline unless it matches MATCH.

Return nil if headline containing point matches MATCH (which
should be a match string of the same format used by
`org-tags-view').  If headline does not match, return the
position of the next headline in current buffer.

Intended for use with `org-agenda-skip-function', where this will
skip exactly those headlines that do not match."
  (save-excursion
    (unless (org-at-heading-p) (org-back-to-heading))
    (let ((next-headline (save-excursion
                           (or (outline-next-heading) (point-max)))))
      (if (my/org-match-at-point-p match) nil next-headline))))

(defun my/org-scheduled-get-time ()
  (let ((scheduled (org-get-scheduled-time (point))))
    (if scheduled
        (format-time-string "%Y-%m-%d" scheduled)
      "")))

(setq org-agenda-hide-tags-regexp (rx (or "org" "refile" "proj" "habit")))

(setq org-agenda-custom-commands
      `(("p" "My outline"
         ((agenda "" ((org-agenda-skip-function '(my/org-agenda-skip-without-match "-habit"))))
          (tags-todo "inbox"
                     ((org-agenda-overriding-header "Inbox")
                      (org-agenda-prefix-format " %i %-12:c")
                      (org-agenda-hide-tags-regexp ".")))
          (tags-todo "+waitlist+SCHEDULED<=\"<+14d>\""
                     ((org-agenda-overriding-header "Waitlist")
                      (org-agenda-hide-tags-regexp "waitlist")
                      (org-agenda-prefix-format " %i %-12:c %-12(my/org-scheduled-get-time)")))
          (tags-todo "habit+SCHEDULED<=\"<+0d>\""
                     ((org-agenda-overriding-header "Habits")
                      (org-agenda-prefix-format " %i %-12:c")
                      (org-agenda-hide-tags-regexp ".")))))))

(use-package org-yaap
  :straight (org-yaap :type git :host gitlab :repo "SqrtMinusOne/org-yaap")
  :after (org)
  :if (not my/nested-emacs)
  :disabled t
  :config
  (org-yaap-mode 1)
  (setq org-yaap-alert-before '(10 1))
  (setq org-yaap-alert-title "PROXIMITY ALERT")
  (setq org-yaap-todo-keywords-only '("FUTURE")))

(setq my/org-alert-notify-times '(600 60))

(setq my/org-alert--alerts (make-hash-table :test #'equal))

(defun my/org-alert--is-scheduled (label time)
  "Check if LABEL is scheduled to be shown an TIME."
  (gethash (cons label time)
           my/org-alert--alerts nil))

(defun my/org-alert--schedule (label time)
  "Schedule LABEL to be shown at TIME, unless it's already scheduled."
  (unless (my/org-alert--is-scheduled label time)
    (puthash (cons label time)
             (run-at-time time
                          nil
                          (lambda ()
                            (alert label
                                   :title "PROXIMITY ALERT")))
             my/org-alert--alerts)))

(defun my/org-alert-cleanup (&optional keys)
  "Unschedule items that do not appear in KEYS.

KEYS is a list of cons cells like (<label> . <time>)."
  (let ((existing-hash (make-hash-table :test #'equal)))
    (cl-loop for key in keys
             do (puthash key t existing-hash))
    (cl-loop for key being the hash-keys of my/org-alert--alerts
             unless (gethash key existing-hash)
             do (progn
                  (cancel-timer (gethash key my/org-alert--alerts))
                  (remhash key my/org-alert--alerts)))))

(defun my/org-alert--update-today-alerts ()
  (when-let* ((files (org-agenda-files))
              (items
               (org-ql-query
                 :select 'element
                 :from files
                 :where `(and
                          (todo "FUTURE")
                          (ts-active :from ,(format-time-string "%Y-%m-%d %H:%M")
                                     :to ,(format-time-string
                                           "%Y-%m-%d"
                                           (time-add
                                            (current-time)
                                            (* 60 60 24)))
                                     :with-time t))
                 :order-by 'date)))
    (let (scheduled-keys)
      (cl-loop
       for item in items
       for scheduled = (org-timestamp-to-time (org-element-property :scheduled item))
       do (cl-loop
           for before-time in my/org-alert-notify-times
           for label = (format "%s at %s [%s min. remaining]"
                               (org-element-property :raw-value item)
                               (format-time-string "%H:%M" scheduled)
                               (number-to-string (/ before-time 60)))
           for time = (time-convert
                       (+ (time-convert scheduled 'integer) (- before-time)))
           do (progn
                (my/org-alert--schedule label time)
                (push (cons label time) scheduled-keys))))
      (my/org-alert-cleanup scheduled-keys))))

(setq my/org-alert--timer nil)

(define-minor-mode my/org-alert-mode ()
  :global t
  :after-hook
  (if my/org-alert-mode
      (progn
        (my/org-alert--update-today-alerts)
        (when (timerp my/org-alert--timer)
          (cancel-timer my/org-alert--timer))
        (setq my/org-alert--timer
              (run-at-time 600 t #'my/org-alert--update-today-alerts)))
    (when (timerp my/org-alert--timer)
      (cancel-timer my/org-alert--timer))
    (my/org-alert-cleanup)))

(with-eval-after-load 'org
  (if my/emacs-started
      (my/org-alert-mode)
    (add-hook 'emacs-startup-hook #'my/org-alert-mode)))

(defun my/org-clone-subtree-with-time-shift (n &optional shift)
  (interactive "nNumber of clones to produce: ")
  (unless (wholenump n) (user-error "Invalid number of replications %s" n))
  (when (org-before-first-heading-p) (user-error "No subtree to clone"))
  (let* ((beg (save-excursion (org-back-to-heading t) (point)))
         (end-of-tree (save-excursion (org-end-of-subtree t t) (point)))
         (shift
          (or shift
              (if (and (not (equal current-prefix-arg '(4)))
                       (save-excursion
                         (goto-char beg)
                         (re-search-forward org-ts-regexp-both end-of-tree t)))
                  (read-from-minibuffer
                   "Date shift per clone (e.g. +1w, empty to copy unchanged): ")
                "")))                   ;No time shift
         (doshift
          (and (org-string-nw-p shift)
               (or (string-match "\\`[ \t]*\\([+-]?[0-9]+\\)\\([hdwmy]\\)[ \t]*\\'"
                                 shift)
                   (user-error "Invalid shift specification %s" shift)))))
    (goto-char end-of-tree)
    (unless (bolp) (insert "\n"))
    (let* ((end (point))
           (template (buffer-substring beg end))
           (shift-n (and doshift (string-to-number (match-string 1 shift))))
           (shift-what (pcase (and doshift (match-string 2 shift))
                         (`nil nil)
                         ("h" 'hour)
                         ("d" 'day)
                         ("w" (setq shift-n (* 7 shift-n)) 'day)
                         ("m" 'month)
                         ("y" 'year)
                         (_ (error "Unsupported time unit"))))
           (nmin 1)
           (nmax n)
           (n-no-remove -1)
           (org-id-overriding-file-name (buffer-file-name (buffer-base-buffer)))
           (idprop (org-entry-get beg "ID")))
      (when (and doshift
                 (string-match-p "<[^<>\n]+ [.+]?\\+[0-9]+[hdwmy][^<>\n]*>"
                                 template))
        (delete-region beg end)
        (setq end beg)
        (setq nmin 0)
        (setq nmax (1+ nmax))
        (setq n-no-remove nmax))
      (goto-char end)
      (cl-loop for n from nmin to nmax do
               (insert
                ;; Prepare clone.
                (with-temp-buffer
                  (insert template)
                  (org-mode)
                  (goto-char (point-min))
                  (org-show-subtree)
                  (and idprop (if org-clone-delete-id
                                  (org-entry-delete nil "ID")
                                (org-id-get-create t)))
                  (unless (= n 0)
                    (while (re-search-forward org-clock-line-re nil t)
                      (delete-region (line-beginning-position)
                                     (line-beginning-position 2)))
                    (goto-char (point-min))
                    (while (re-search-forward org-drawer-regexp nil t)
                      (org-remove-empty-drawer-at (point))))
                  (goto-char (point-min))

                  (when doshift
                    (while (re-search-forward org-ts-regexp-both nil t)
                      (org-timestamp-change (* n shift-n) shift-what))
                    (save-excursion
                      (goto-char (point-min))
                      (evil-numbers/inc-at-pt n (point-min)))
                    (unless (= n n-no-remove)
                      (goto-char (point-min))
                      (while (re-search-forward org-ts-regexp nil t)
                        (save-excursion
                          (goto-char (match-beginning 0))
                          (when (looking-at "<[^<>\n]+\\( +[.+]?\\+[0-9]+[hdwmy]\\)")
                            (delete-region (match-beginning 1) (match-end 1)))))))
                  (buffer-string)))))
    (goto-char beg)))

(defun my/org--headings-in-outline ()
  (org-ql-query
    :select (lambda () (propertize
                        (substring-no-properties (org-get-heading t t t))
                        'marker (copy-marker (point))))
    :from (append
           (list (buffer-file-name))
           (let ((archive
                  (concat (file-name-directory (buffer-file-name))
                          "archive/"
                          (file-name-nondirectory (buffer-file-name)))))
             (when (file-exists-p archive)
               (list archive))))
    :where `(and (outline-path ,@(org-get-outline-path))
                 (level ,(org-current-level)))))

(defun my/org--heading-strip (heading)
  (thread-last
    heading
    (substring-no-properties)
    (replace-regexp-in-string (rx (| "(" "[") (+ nonl) (| "]" ")")) "")
    (replace-regexp-in-string (rx " " (+ (or digit "."))) " ")
    (replace-regexp-in-string (rx (+ " ")) " ")
    (string-trim)))

(defun my/org--headings-group-seq (headings)
  (thread-last
    headings
    (seq-group-by #'my/org--heading-strip)
    (seq-sort-by #'car #'string-lessp)
    (mapcar (lambda (group)
              (cons (car group)
                    (seq-sort-by
                     (lambda (heading)
                       (save-match-data
                         (or
                          (and (string-match (rx (group (+ digit)))
                                             heading)
                               (string-to-number (match-string 1 heading)))
                          -1)))
                     #'<
                     (cdr group)))))))

(defun my/org-headings-seq ()
  (interactive)
  (let* ((headings (my/org--headings-in-outline))
         (headings-seq (my/org--headings-group-seq headings))
         (buffer (generate-new-buffer "*Sequential Headings in Outline*")))
    (with-current-buffer buffer
      (outline-mode)
      (setq-local widget-push-button-prefix "")
      (setq-local widget-push-button-suffix "")
      (dolist (group headings-seq)
        (insert (format "* %s\n" (car group)))
        (dolist (heading (cdr group))
          (widget-create 'push-button
                         :marker (get-text-property 0 'marker heading)
                         :notify (lambda (widget &rest ignore)
                                   (let ((marker (widget-get widget :marker)))
                                     (pop-to-buffer (marker-buffer marker))
                                     (goto-char marker)))
                         (concat "** " (substring-no-properties heading)))
          (insert "\n")))
      (widget-setup)
      (setq buffer-read-only t)
      (goto-char (point-min)))
    (pop-to-buffer buffer)))

(defun my/org-heading-seq-insert ()
  (interactive)
  (let* ((headings (my/org--headings-in-outline))
         (headings-seq (my/org--headings-group-seq headings))
         (heading (completing-read "Headings: " headings-seq))
         (last-number
          (thread-last headings-seq
                       (assoc heading)
                       (cdr)
                       (mapcar (lambda (x)
                                 (save-match-data
                                   (or
                                    (when (string-match (rx (group (+ digit)))
                                                        x)
                                      (string-to-number (match-string 1 x)))
                                    1))))
                       (seq-max)
                       (1+))))
    (org-insert-heading '(4))
    (insert (format "FUTURE %s %s" heading last-number))))

(defun my/org-archive--get-file ()
  "Get an archive version of the file."
  (let ((archive-file
         (concat
          (file-name-directory (buffer-file-name))
          "archive/" (file-name-nondirectory (buffer-file-name)))))
    (unless (file-exists-p archive-file)
      (make-empty-file archive-file))
    archive-file))

(defun my/org-refile--assert-path-exists (refile-path)
  (cl-assert (equal org-refile-use-outline-path 'file))
  (let* ((parts (string-split refile-path "/"))
         (tbl (mapcar
               (lambda (x)
                 (cons (concat (car x) "/") (cdr x)))
               org-refile-target-table)))
    (cl-loop for i from 1
             for part in (cdr parts)
             for target = (org-refile--get-location
                           (string-join (seq-take parts (1+ i)) "/")
                           tbl)
             unless target
             do (let ((parent-target
                       (org-refile--get-location
                        (string-join (seq-take parts i) "/")
                        tbl)))
                  (push (org-refile-new-child parent-target part) tbl)))))

(defun my/org-archive-refile ()
  (interactive)
  (let* ((org-refile-targets `((,(my/org-archive--get-file) . (:maxlevel . 6))))
         (org-refile-target-table (org-refile-get-targets))
         (org-refile-history nil)
         (org-refile-use-outline-path 'file)
         (org-refile-allow-creating-parent-nodes t)
         (org-outline-path-complete-in-steps nil)
         (refile-path (string-join
                       (append
                        (list (file-name-nondirectory
                               (buffer-file-name)))
                        (org-get-outline-path nil t))
                       "/")))
    ;; The path is already known
    (cl-letf (((symbol-function 'completing-read)
               (lambda (&rest _) refile-path)))
      (my/org-refile--assert-path-exists refile-path)
      (org-refile))))

(defun my/org-archive-refile-all (days)
  (interactive (list (read-number "Days: " 60)))
  (let ((records (org-ql-query
                   :select #'element-with-markers
                   :from (current-buffer)
                   :where `(and (ts :to ,(- days)) (done)))))
    (when (y-or-n-p (format "Archive %d records? " (length records)))
      (dolist (record records)
        (let ((marker (org-element-property :org-marker record)))
          (org-with-point-at marker
            (my/org-archive-refile)))))))

(my-leader-def
  :infix "o"
  "" '(:which-key "org-mode")
  "c" 'org-capture
  "a" 'org-agenda
  "o" #'my/org-file-open
  "v" #'org-ql-view
  "q" #'org-ql-search)

(with-eval-after-load 'org
  (my-leader-def
    :infix "SPC"
    :keymaps '(org-mode-map)
    "i" #'org-clock-in
    "o" #'org-clock-out
    "O" #'org-clock-cancel
    "c" #'org-clock-goto
    "p" #'org-set-property
    "e" #'org-set-effort
    "r" #'org-priority
    "m" #'my/org-meeting-link))

(use-package org-journal
  :straight t
  :if (not my/remote-server)
  :init
  (my-leader-def
    :infix "oj"
    "" '(:which-key "org-journal")
    "j" 'org-journal-new-entry
    "o" 'org-journal-open-current-journal-file
    "s" 'org-journal-tags-status)
  :after org
  :config
  (setq org-journal-dir (concat org-directory "/journal"))
  (setq org-journal-file-type 'weekly)
  (setq org-journal-file-format "%Y-%m-%d.org")
  (setq org-journal-date-format "%A, %Y-%m-%d")
  (setq org-journal-enable-encryption t)
  (setq org-journal-time-format-post-midnight "PM: %R "))

(use-package org-journal-tags
  :straight (:host github :repo "SqrtMinusOne/org-journal-tags")
  :after (org-journal)
  :if (not my/remote-server)
  :config
  (org-journal-tags-autosync-mode)
  (general-define-key
   :keymaps 'org-journal-mode-map
   "C-c t" #'org-journal-tags-insert-tag))

(use-package request
  :straight t
  :defer t)

(defvar my/weather-last-time 0)
(defvar my/weather-value nil)

(defun my/weather-get ()
  (when (> (- (time-convert nil 'integer) my/weather-last-time)
           (* 60 5))
    (request (format "https://wttr.in/%s" my/location)
      :params '(("format" . "%l:%20%C%20%t%20%w%20%p"))
      :sync t
      :parser (lambda () (url-unhex-string (buffer-string)))
      :timeout 10
      :success (cl-function
                (lambda (&key data &allow-other-keys)
                  (setq my/weather-value data)
                  (setq my/weather-last-time (time-convert nil 'integer))))
      :error
      (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
                     (message "Got error: %S" error-thrown)))))
  my/weather-value)

(defun my/get-mood ()
  (let* ((crm-separator " ")
         (crm-local-completion-map
          (let ((map (make-sparse-keymap)))
            (set-keymap-parent map crm-local-completion-map)
            (define-key map " " 'self-insert-command)
            map))
         (vertico-sort-function nil))
    (mapconcat
     #'identity
     (completing-read-multiple
      "How do you feel: "
      my/mood-list)
     " ")))

(defun my/set-journal-header ()
  (org-set-property "Emacs" emacs-version)
  (org-set-property "Hostname" (my/system-name))
  (org-journal-tags-prop-apply-delta :add (list (format "host.%s" (my/system-name))))
  (when (boundp 'my/location)
    (org-set-property "Location" my/location)
    (when-let ((weather (my/weather-get)))
      (org-set-property "Weather" weather)))
  (when (boundp 'my/loc-tag)
    (org-journal-tags-prop-apply-delta :add (list my/loc-tag)))
  (when (fboundp 'emms-playlist-current-selected-track)
    (let ((track (emms-playlist-current-selected-track)))
      (when track
        (let ((album (cdr (assoc 'info-album track)))
              (artist (or (cdr (assoc 'info-albumartist track))
                          (cdr (assoc 'info-album track))))
              (title (cdr (assoc 'info-title track)))
              (string ""))
          (when artist
            (setq string (concat string "[" artist "] ")))
          (when album
            (setq string (concat string album " - ")))
          (when title
            (setq string (concat string title)))
          (when (> (length string) 0)
            (org-set-property "EMMS_Track" string))))))
  (when-let (mood (my/get-mood))
    (org-set-property "Mood" mood)))

(add-hook 'org-journal-after-entry-create-hook
          #'my/set-journal-header)

(defun my/org-journal-decrypt ()
  "Decrypt the current org journal file."
  (interactive)
  (org-journal-tags--ensure-decrypted))

(use-package citar
  :straight t
  :init
  (my-leader-def "fB" #'citar-open)
  :commands (citar-open citar-insert-citation)
  :config
  (setq
   org-cite-global-bibliography '("~/30-39 Life/32 org-mode/library.bib")
   org-cite-insert-processor 'citar
   org-cite-follow-processor 'citar
   org-cite-activate-processor 'citar
   citar-bibliography org-cite-global-bibliography)
  (setq org-cite-export-processors
        '((latex bibtex "numeric")))
  (setq citar-library-paths
        '("~/30-39 Life/33 Library/33.01 Documents/"))
  (add-hook 'latex-mode #'citar-capf-setup)
  (add-hook 'org-mode #'citar-capf-setup))

(use-package citar-embark
  :after (citar embark)
  :straight t
  :config
  (citar-embark-mode))

(use-package org-ref
  :straight (:files (:defaults "citeproc" (:exclude "*helm*")))
  :if (not my/remote-server)
  :commands (org-ref-insert-link-hydra/body
             org-ref-bibtex-hydra/body)
  :init
  (setq bibtex-dialect 'biblatex)
  (add-hook 'bibtex-mode 'smartparens-mode)
  :after (org)
  :config
  (general-define-key
   :keymaps 'org-mode-map
   "C-c l" #'org-ref-insert-link-hydra/body)
  (general-define-key
   :keymaps 'bibtex-mode-map
   "M-RET" 'org-ref-bibtex-hydra/body)
  (setq org-ref-insert-cite-function
        (lambda ()
          (call-interactively #'citar-insert-citation))))

(use-package emacsql-sqlite
  :defer t
  :if (not my/remote-server)
  :straight (:type built-in))

(use-package org-roam
  :straight (:host github :repo "org-roam/org-roam"
                   :files (:defaults "extensions/*.el"))
  :if (and
       (not my/remote-server)
       (file-directory-p org-roam-directory))
  :after org
  :init
  (setq org-roam-file-extensions '("org"))
  (setq org-roam-v2-ack t)
  (setq org-roam-node-display-template (concat "${title:*} " (propertize "${tags:10}" 'face 'org-tag)))
  :config
  (org-roam-setup)
  (require 'org-roam-protocol))

(setq org-roam-capture-templates
      `(("d" "default" plain "%?"
         :target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n")
         :unnarrowed t)
        ("f" "fleeting" plain "%?"
         :target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+filetags: :fleeting:\n")
         :unnarrowed t)
        ("e" "encrypted" plain "%?"
         :target (file+head "%<%Y%m%d%H%M%S>-${slug}.org.gpg" "#+title: ${title}\n")
         :unnarrowed t)))

(use-package org-roam-ql
  :straight t
  :after (org-roam)
  :config
  (general-define-key
   :states '(normal visual)
   :keymaps '(org-roam-ql-mode-map)
   "s" #'org-roam-ql-buffer-dispatch))

(defun my/org-roam-node-find-permanent (&optional other-window)
  (interactive current-prefix-arg)
  (org-roam-node-find
   other-window
   nil
   (lambda (node)
     (not
      (seq-contains-p
       "fleeting"
       (org-roam-node-tags node))))))

(defun my/org-roam-node-insert-permanent ()
  (interactive)
  (org-roam-node-insert
   (lambda (node)
     (not
      (seq-contains-p
       (org-roam-node-tags node)
       "fleeting")))))

(defun my/org-roam-ql-fleeting ()
  (interactive)
  (org-roam-ql-search
   '(tags "fleeting")
   "Fleeting notes"))

(with-eval-after-load 'org-roam
  (my-leader-def
    :infix "or"
    "" '(:which-key "org-roam")
    "i" #'my/org-roam-node-insert-permanent
    "r" #'my/org-roam-node-find-permanent
    "g" #'org-roam-graph
    "c" #'org-roam-capture
    "b" #'org-roam-buffer-toggle
    "q" #'org-roam-ql-search
    "f" #'my/org-roam-ql-fleeting)
  (general-define-key
   :keymaps 'org-roam-mode-map
   :states '(normal)
   "TAB" #'magit-section-toggle
   "q" #'quit-window
   "k" #'magit-section-backward
   "j" #'magit-section-forward
   "gr" #'revert-buffer
   "RET" #'org-roam-buffer-visit-thing))

(with-eval-after-load 'org
  (my-leader-def
    :keymap 'org-mode-map
    :infix "or"
    "t" #'org-roam-tag-add
    "T" #'org-roam-tag-remove
    "s" #'org-roam-db-autosync-mode
    "a" #'org-roam-alias-add)
  (general-define-key
   :keymap 'org-mode-map
   "C-c i" #'my/org-roam-node-insert-permanent
   "C-c I" #'org-roam-node-insert))

(defface my/org-roam-count-overlay-face
  '((t :inherit tooltip))
  "Face for Org Roam count overlay.")

(defun my/org-roam--count-overlay-make (pos count)
  (let* ((overlay-value (concat
                         " "
                         (propertize
                          (format "%d" count)
                          'face 'my/org-roam-count-overlay-face)
                         " "))
         (ov (make-overlay pos pos (current-buffer) nil t)))
    (overlay-put ov 'roam-backlinks-count count)
    (overlay-put ov 'priority 1)
    (overlay-put ov 'after-string overlay-value)))

(defun my/org-roam--count-overlay-remove-all ()
  (dolist (ov (overlays-in (point-min) (point-max)))
    (when (overlay-get ov 'roam-backlinks-count)
      (delete-overlay ov))))

(defun my/org-roam--count-overlay-make-all ()
  (my/org-roam--count-overlay-remove-all)
  (org-element-map (org-element-parse-buffer) 'link
    (lambda (elem)
      (when (string-equal (org-element-property :type elem) "id")
        (let* ((id (org-element-property :path elem))
               (count (caar
                       (org-roam-db-query
                        [:select (funcall count source)
                         :from links
                         :where (= dest $s1)
                         :and (= type "id")]
                        id))))
          (when (< 0 count)
            (my/org-roam--count-overlay-make
             (org-element-property :end elem)
             count)))))))

(define-minor-mode my/org-roam-count-overlay-mode
  "Display backlink count for org-roam links."
  :after-hook
  (if my/org-roam-count-overlay-mode
      (progn
        (my/org-roam--count-overlay-make-all)
        (add-hook 'after-save-hook #'my/org-roam--count-overlay-make-all nil t))
    (my/org-roam--count-overlay-remove-all)
    (remove-hook 'after-save-hook #'my/org-roam--count-overlay-remove-all t)))

(defun my/org-roam-extract-links ()
  (interactive)
  (let ((buffer (generate-new-buffer "*roam-links*"))
        elems)
    (org-element-map (org-element-parse-buffer) 'link
      (lambda (elem)
        (when (string-equal (org-element-property :type elem) "id")
          (push elem elems))))
    (with-current-buffer buffer
      (cl-loop for elem in elems
               for file-name =
               (file-name-nondirectory
                (caar
                 (org-roam-db-query
                  [:select [file]
                           :from nodes
                           :where (= id $s1)]
                  (org-element-property :path elem))))
               do (insert file-name "\n")))
    (switch-to-buffer buffer)))

(use-package org-roam-ui
  :straight (:host github :repo "org-roam/org-roam-ui" :branch "main" :files ("*.el" "out"))
  :if (not my/remote-server)
  :after org-roam
  ;; :hook (org-roam . org-roam-ui-mode)
  :init
  (my-leader-def "oru" #'org-roam-ui-mode))

(use-package deft
  :straight t
  :if (not my/remote-server)
  :commands (deft)
  :after (org)
  :init
  (my-leader-def "ord" #'deft)
  :config
  (setq deft-directory org-roam-directory)
  (setq deft-recursive t)
  (setq deft-use-filter-string-for-filename t)
  (add-hook 'deft-mode-hook
            (lambda () (display-line-numbers-mode -1)))
  (general-define-key
   :keymaps 'deft-mode-map
   :states '(normal motion)
   "q" #'quit-window
   "r" #'deft-refresh
   "s" #'deft-filter
   "d" #'deft-filter-clear
   "y" #'deft-filter-yank
   "t" #'deft-toggle-incremental-search
   "o" #'deft-toggle-sort-method))

(setq deft-strip-summary-regexp
      (rx (or
           (: ":PROPERTIES:" (* anything) ":END:")
           (: "#+" (+ alnum) ":" (* nonl))
           (regexp "[\n\t]"))))

(defun my/deft-parse-summary-around (fun contents title)
  (funcall fun (org-link-display-format contents) title))

(with-eval-after-load 'deft
  (advice-add #'deft-parse-summary :around #'my/deft-parse-summary-around))

(defun my/deft-parse-title (file contents)
  (with-temp-buffer
    (insert contents)
    (goto-char (point-min))
    (if (search-forward-regexp (rx (| "#+title:" "#+TITLE:")) nil t)
        (string-trim (buffer-substring-no-properties (point) (line-end-position)))
      file)))

(defun my/deft-parse-title-around (fun file contents)
  (or (my/deft-parse-title file contents)
      (funcall fun file contents)))

(with-eval-after-load 'deft
  (advice-add #'deft-parse-title :around #'my/deft-parse-title-around))

(defun my/org-roam-node-setup ()
  (setq-local org-hide-emphasis-markers t)
  (org-appear-mode 1)
  (when (display-graphic-p)
    (org-fragtog-mode 1)
    (org-latex-preview '(16))))

(with-eval-after-load 'org
  (add-hook 'org-roam-find-file-hook 'my/org-roam-node-setup))

(setq my/git-diff-status
      '(("A" . added)
        ("C" . copied)
        ("D" . deleted)
        ("M" . modified)
        ("R" . renamed)
        ("R100" . moved)
        ("T" . type-changed)
        ("U" . unmerged)))

(defun my/get-files-status (rev)
  (let ((files (shell-command-to-string (concat "git diff --name-status " rev))))
    (mapcar
     (lambda (file)
       (let ((elems (split-string file "\t")))
         (cons
          (cdr (assoc (car elems) my/git-diff-status))
          (car (last elems)))))
     (split-string files "\n" t))))

(defun my/org-changed-files-since-date (date)
  (let ((default-directory org-directory))
    (my/get-files-status (format "@{%s}" date))))

(defun my/org-review--org-roam-get-changes (date)
  (let ((changes (my/org-changed-files-since-date date))
        (nodes (org-roam-node-list))
        (nodes-by-file (make-hash-table :test #'equal)))
    (cl-loop for node in nodes
             for file = (org-roam-node-file node)
             do (puthash file node nodes-by-file))
    (let* ((changed-nodes
            (thread-last
              changes
              (mapcar (lambda (c)
                        (cons (car c)
                              (gethash
                               (concat org-directory "/" (cdr c))
                               nodes-by-file))))
              (seq-filter #'cdr)))
           (changed-inbox
            (thread-last
              changes
              (seq-filter
               (lambda (file) (string-match-p (rx bos "inbox-notes") (cdr file))))))
           (changed-fleeting
            (thread-last
              changed-nodes
              (seq-filter (lambda (c)
                            (seq-contains-p (org-roam-node-tags (cdr c))
                                            "fleeting")))
              (seq-sort-by (lambda (c) (concat (symbol-name (car c))
                                               (org-roam-node-title (cdr c))))
                           #'string-lessp)))
           (changed-permanent
            (thread-last
              changed-nodes
              (seq-filter (lambda (c)
                            (not (seq-contains-p (org-roam-node-tags (cdr c))
                                                 "fleeting"))))
              (seq-sort-by (lambda (c) (concat (symbol-name (car c))
                                               (org-roam-node-title (cdr c))))
                           #'string-lessp))))
      (list
       changed-inbox
       changed-fleeting
       changed-permanent))))

(defun my/org-review-org-roam-format (date)
  (let* ((data (my/org-review--org-roam-get-changes date))
         (changed-inbox (nth 0 data))
         (changed-fleeting (nth 1 data))
         (changed-permanent (nth 2 data)))
    (concat
     "Changes in inbox:\n"
     (thread-last
       changed-inbox
       (mapcar (lambda (change)
                 (format "- %s :: %s\n"
                         (cond
                          ((or (member (car change) '(deleted moved))
                               (string-match-p "figured-out" (cdr change)))
                           "Processed")
                          (t (capitalize (symbol-name (car change)))))
                         (cdr change))))
       (apply #'concat))
     "\nChanges in fleeting notes:\n"
     (thread-last
       changed-fleeting
       (mapcar (lambda (c)
                 (format "- %s :: [[id:%s][%s]]\n"
                         (capitalize (symbol-name (car c)))
                         (org-roam-node-id (cdr c))
                         (org-roam-node-title (cdr c)))))
       (apply #'concat))
     "\nChanges in permanent notes:\n"
     (thread-last
       changed-permanent
       (mapcar (lambda (c)
                 (format "- %s :: [[id:%s][%s]]\n"
                         (capitalize (symbol-name (car c)))
                         (org-roam-node-id (cdr c))
                         (org-roam-node-title (cdr c)))))
       (apply #'concat)))))

(defun my/org-review-get-last-review-date (kind)
  (let* ((start-of-day (- (time-convert nil #'integer)
                          (% (time-convert nil #'integer)
                             (* 24 60 60))))
         (query-res (org-journal-tags-query
                     :tag-names (list (format "review.%s" kind))
                     :start-date (pcase kind
                                   ('weekly
                                    (- start-of-day
                                       (* 21 24 60 60)))
                                   ('zk
                                    (- start-of-day
                                       (* 45 24 60 60)))
                                   (_ (error "Unsupported kind: %s" kind)))
                     :location 'section
                     :order 'descending)))
    (if query-res
        (org-journal-tag-reference-date (car query-res))
      (pcase kind
        ('weekly (- start-of-day (* 7 24 60 60)))
        ('zk (- start-of-day (* 45 24 60 60)))))))

(defun my/org-review-set-weekly-record ()
  (save-excursion
    (let ((last-review-date (my/org-review-get-last-review-date 'weekly)))
      (org-journal-tags-prop-apply-delta :add '("review.weekly"))
      (insert "Weekly Review")
      (goto-char (point-max))

      (insert "Last review date: "
              (format-time-string
               "[%Y-%m-%d]"
               (seconds-to-time last-review-date)))
      (insert "

Review checklist (/delete this/):
- [ ] Clear email inbox
- [ ] Reconcile ledger
- [ ] Clear [[file:~/Downloads][downloads]] and [[file:~/00-Scratch][scratch]] folders
- [ ] Process [[file:~/30-39 Life/35 Photos/35.00 Inbox/][photo inbox]]
- [ ] Process [[file:../inbox.org][inbox]]
- [ ] Create [[file:../recurring.org][recurring tasks]] for next week
- [ ] Check agenda (-1 / +2 weeks): priorities, deadlines
- [ ] Check TODOs: priorities, deadlines
  - [[org-ql-search:todo%3A?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: All TODOs]]
  - [[org-ql-search:(and (todo) (not (tags \"nots\")) (not (ts :from -14)) (not (todo \"MAYBE\")))?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: Stale tasks]]
  - [[org-ql-search:todo%3AWAIT?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: WAIT]]
  - [[org-ql-search:todo%3AMAYBE?buffers-files=%22org-agenda-files%22&super-groups=%28%28%3Aauto-outline-path-file%20t%29%29&sort=%28priority%20todo%20deadline%29][org-ql-search: MAYBE]]
- [ ] Run auto-archiving
- [ ] Review journal records
")

      (insert "
*** Summary
TODO Write something, maybe? "))))

(defun my/org-review-weekly ()
  (interactive)
  (let ((org-journal-after-entry-create-hook
         `(,@org-journal-after-entry-create-hook
           my/org-review-set-weekly-record)))
    (org-journal-new-entry nil)
    (org-fold-show-subtree)))

(with-eval-after-load 'org-journal
  (my-leader-def "ojw" #'my/org-review-weekly))

(defun my/kill-messengers ()
  (interactive)
  (when (get-buffer telega-root-buffer-name)
    (telega-kill t))
  (call-process-shell-command "pkill -f rocketchat-desktop")
  (call-process-shell-command "pkill -f 'bwrap --args 36 element'")
  (call-process-shell-command "pkill -f element-desktop"))

(defun my/org-review-set-daily-record ()
  (let* ((today (format-time-string
                 "%Y-%m-%d"
                 (days-to-time
                  (- (org-today) (time-to-days 0)))))
         (roam-changes (my/org-review--org-roam-get-changes today)))
    (save-excursion
      (org-journal-tags-prop-apply-delta :add '("review.daily"))
      (insert "Daily Review")
      (goto-char (point-max))

      (insert "
Maintenance checklist (/delete this/):
- [ ] [[elisp:(my/kill-messengers)][Close all messengers]]
- [ ] Process [[file:../inbox.org][inbox]]
- [ ] Check if clocked tasks are properly annotated
  - [[elisp:(my/org-ql-clocked-today)][Tasks clocked today]]
  - [[elisp:(my/org-ql-closed-today)][Tasks closed today]]
- [ ] Check agenda for the current week

/Remember, all of the following headers are optional./

*** Happened today
Happened to me:
- /Anything interesting?/
Happened to the world:
- /Anything important?/

*** New ideas
/Write them down in org-roam with the \"fleeting\" tag; leave links here. Perhaps note what sparked that idea?/
"
              (thread-last
                (nth 1 roam-changes)
                (seq-filter (lambda (c) (eq 'added (car c))))
                (mapcar (lambda (c)
                          (format "- [[id:%s][%s]]\n"
                                  (org-roam-node-id (cdr c))
                                  (org-roam-node-title (cdr c)))))
                (apply #'concat))
              "
*** Interactions today
/Any meaninginful interactions, conflicts or tensions?/

*** Emotions today
/How did I feel?/
"))))

(defun my/org-review-daily ()
  (interactive)
  (let ((org-journal-after-entry-create-hook
         `(,@org-journal-after-entry-create-hook
           my/org-review-set-daily-record)))
    (org-journal-new-entry nil)
    (org-fold-show-subtree)))

(with-eval-after-load 'org-journal
  (my-leader-def "ojd" #'my/org-review-daily))

(defun my/org-review-org-roam-format-zk-before (date)
  (let* ((data (my/org-review--org-roam-get-changes date))
         (changed-inbox (nth 0 data))
         (changed-fleeting (nth 1 data))
         (changed-permanent (nth 2 data)))
    (concat
     (when changed-inbox
       (concat
        "Process these changes in inbox:\n"
        (thread-last
          changed-inbox
          (mapcar (lambda (change)
                    (format "- [ ] %s :: %s\n"
                            (cond
                             ((or (member (car change) '(deleted moved))
                                  (string-match-p "figured-out" (cdr change)))
                              "Processed")
                             (t (capitalize (symbol-name (car change)))))
                            (cdr change))))
          (apply #'concat))
        "\n"))
     (when changed-fleeting
       (concat
        "Process these fleeting notes:\n"
        (thread-last
          changed-fleeting
          (mapcar (lambda (c)
                    (format "- [ ] %s :: [[id:%s][%s]]\n"
                            (capitalize (symbol-name (car c)))
                            (org-roam-node-id (cdr c))
                            (org-roam-node-title (cdr c)))))
          (apply #'concat))
        "\n"))
     (when changed-permanent
       (concat
        "Check these changes in permanent notes:\n"
        (thread-last
          changed-permanent
          (mapcar (lambda (c)
                    (format "- [ ] %s :: [[id:%s][%s]]\n"
                            (capitalize (symbol-name (car c)))
                            (org-roam-node-id (cdr c))
                            (org-roam-node-title (cdr c)))))
          (apply #'concat)))))))

(defun my/org-review-org-roam-finish (date)
  (org-roam-db-sync)
  (save-excursion
    (org-back-to-heading)
    (replace-regexp
     (rx
      ":BEGIN_REVIEW:" (* anything) ":END_REVIEW:")
     (string-trim
      (my/org-review-org-roam-format date)))))

(defun my/org-review-set-zk-record ()
  (save-excursion
    (let ((last-review-date (my/org-review-get-last-review-date 'zk)))
      (org-journal-tags-prop-apply-delta :add '("review.zk"))
      (insert "Zettelkasten Review")
      (goto-char (point-max))

      (insert "Last review date: "
              (format-time-string
               "[%Y-%m-%d]"
               (seconds-to-time last-review-date)))

      (insert "\n\n:BEGIN_REVIEW:\n"
              "Process all the required categories in this block, then execute \"Finish review\".\n\n"
              (string-trim
               (my/org-review-org-roam-format-zk-before last-review-date))
              "\n\n[[elisp:(my/org-review-org-roam-finish \""
              (format-time-string "%Y-%m-%d" last-review-date)
              "\")][Finish review]]"
              "\n:END_REVIEW:\n"))))

(defun my/org-review-zk ()
  (interactive)
  (let ((org-journal-after-entry-create-hook
         `(,@org-journal-after-entry-create-hook
           my/org-review-set-zk-record)))
    (org-journal-new-entry nil)
    (org-fold-show-subtree)))

(with-eval-after-load 'org-journal
  (my-leader-def "ojz" #'my/org-review-zk))

(use-package org-contacts
  :straight (:type git :repo "https://repo.or.cz/org-contacts.git")
  :if (not my/remote-server)
  :after (org)
  :config
  (setq org-contacts-files (list
                            (concat org-directory "/contacts.org"))))

(defun my/calfw-setup-buffer ()
  (display-line-numbers-mode -1))

(use-package calfw
  :straight t
  :defer t
  :config
  (add-hook 'cfw:calendar-mode-hook #'my/calfw-setup-buffer))

(use-package calfw-org
  :after (calfw org)
  :straight t)

(defun my/org-timeblock-conf ()
  (display-line-numbers-mode -1))

(use-package org-timeblock
  :straight (:host github :repo "ichernyshovvv/org-timeblock")
  :commands (org-timeblock-mode)
  :init
  (my-leader-def "ot" #'org-timeblock)
  :config
  (add-hook 'org-timeblock-mode-hook #'my/org-timeblock-conf)
  (general-define-key
   :keymaps '(org-timeblock-mode-map)
   :states '(normal visual)
   "j" #'org-timeblock-forward-block
   "h" #'org-timeblock-backward-column
   "l" #'org-timeblock-forward-column
   "k" #'org-timeblock-backward-block
   "M-[" #'org-timeblock-day-earlier
   "M-]" #'org-timeblock-day-later
   "H" #'org-timeblock-day-earlier
   "L" #'org-timeblock-day-later
   "RET" #'org-timeblock-goto
   "t" #'org-timeblock-todo-set
   "q" #'quit-window))

(use-package org-drill
  :straight t
  :commands (org-drill)
  :after (org))

(provide 'sqrt-org-productivity)
