;;; gnosis-nodes.el --- Node management  -*- lexical-binding: t; -*-

;; Copyright (C) 2026  Free Software Foundation, Inc.

;; Author: Thanos Apollo <public@thanosapollo.org>
;; Keywords: extensions

;;; Commentary:

;; Node CRUD, sync, file management, and interactive commands.
;; Uses gnosis-sqlite for DB and gnosis-org for parsing.
;; All node tables live in the unified gnosis database.

;;; Code:

(require 'cl-lib)
(require 'org)
(require 'org-element)
(require 'gnosis-org)
(require 'gnosis-db)
(require 'gnosis-sqlite)
(declare-function gnosis-journal--dir "gnosis-journal")

(defgroup gnosis-nodes nil
  "Gnosis node management."
  :group 'gnosis)

(defcustom gnosis-nodes-dir (expand-file-name "Notes" "~")
  "Directory with gnosis nodes."
  :type 'directory)

(defcustom gnosis-nodes-show-tags nil
  "Display tags with `gnosis-nodes-find'."
  :type 'boolean)

(defcustom gnosis-nodes-timestring "%Y%m%d%H%M%S"
  "Timestring used for the creation of node files.
When nil, filenames use just the title (e.g. \"my_title.org\")."
  :type '(choice (string :tag "Time format string")
		 (const :tag "Title only (no prefix)" nil)))

(defcustom gnosis-nodes-create-as-gpg nil
  "When non-nil, create all node files with a .gpg suffix."
  :type 'boolean)

(defcustom gnosis-nodes-templates
  (list (cons "Empty" (lambda () ""))
        (cons "Annotated"
              (lambda ()
                (concat "{*} Summary\n\n"
                        "{*} Notes\n\n"
                        "{*} References\n")))
        (cons "Reading"
              (lambda ()
                (let ((author (read-string "Author: ")))
                  (concat "{*} Key Ideas\n\n"
                          "{*} Quotes\n\n"
                          "{*} Notes\n"
                          (unless (string-empty-p author)
                            (format "\nAuthor: %s\n" author)))))))
  "Templates for nodes.
Template functions return strings.  Use \"{*}\" as a heading
placeholder; it will be expanded to org heading stars relative to
the insertion context.  \"{**}\" adds one extra level, \"{***}\"
adds two, etc."
  :type '(alist :key-type (string :tag "Name")
                :value-type (function :tag "Template Function")))

(defcustom gnosis-nodes-completing-read-func #'org-completing-read
  "Function to use for `completing-read' in node operations."
  :type 'function)

(defface gnosis-nodes-face-tags
  '((t :inherit font-lock-type-face))
  "Face for displaying tags with `gnosis-nodes-find'.")

;;; DB access wrappers

(defun gnosis-nodes-select (value table &optional restrictions flatten)
  "Select VALUE from TABLE, optionally with RESTRICTIONS.
Optional argument FLATTEN, when non-nil, flattens the result.
Delegates to `gnosis-select'."
  (gnosis-select value table restrictions flatten))

(defun gnosis-nodes--insert-into (table values &optional or-ignore)
  "Insert VALUES to TABLE.
Delegates to `gnosis--insert-into' (unified DB).
When OR-IGNORE, skip rows that violate UNIQUE constraints."
  (gnosis--insert-into table values or-ignore))

(defun gnosis-nodes--delete (table value)
  "From TABLE use where to delete VALUE.
Delegates to `gnosis--delete' (unified DB)."
  (gnosis--delete table value))

(defun gnosis-nodes--all-tags ()
  "Return all unique node tags from the junction table."
  (mapcar #'car
	  (gnosis-sqlite-select (gnosis--ensure-db)
				"SELECT DISTINCT tag FROM node_tag")))

;;; Ensure directories

(defun gnosis-nodes-ensure-directories ()
  "Create node directories if they do not exist."
  (unless (file-directory-p gnosis-nodes-dir)
    (make-directory gnosis-nodes-dir t)))

;;; File operations

(defun gnosis-nodes--update-file (file &optional journal buffer)
  "Add contents of FILE to database.
If JOURNAL is non-nil, update file as a journal entry.
When BUFFER is non-nil, parse it instead of reading FILE from disk.
This avoids re-reading (and re-decrypting) files already open."
  (condition-case err
      (let* ((info (if buffer
		       (with-temp-buffer
			 (insert (with-current-buffer buffer
				   (save-restriction (widen) (buffer-string))))
			 (gnosis-org-get-buffer-info))
		     (gnosis-org-get-file-info file)))
	     (hash (car (last info)))
	     (data (butlast info 2))
	     (table (if journal 'journal 'nodes))
	     (filename (file-name-nondirectory file))
	     (full-path (expand-file-name file (if journal
						   (gnosis-journal--dir)
						 gnosis-nodes-dir)))
	     (mtime (format-time-string "%s" (file-attribute-modification-time
					      (file-attributes full-path))))
	     (links (and (> (length info) 2)
			 (car (last (butlast info))))))
	(message "Parsing: %s" filename)
	(gnosis-sqlite-with-transaction (gnosis--ensure-db)
	  (cl-loop for item in data
		   when (plist-get item :id)
		   do (let ((title (plist-get item :title))
			    (id (plist-get item :id))
			    (tags (plist-get item :tags))
			    (level (plist-get item :level)))
			(gnosis-nodes--insert-into table
						   `([,id ,filename ,title ,level
							  ,(prin1-to-string tags) ,mtime ,hash]))
			;; Insert tags
			(cl-loop for tag in tags
				 do (gnosis-nodes--insert-into 'node-tag `([,id ,tag]) t))
			;; Insert master relationship as link (nodes only)
			(when (and (not journal)
				   (plist-get item :master)
				   (stringp (plist-get item :master)))
			  (gnosis-nodes--insert-into
			   'node-links
			   `([,id ,(plist-get item :master)])
			   t))))
	  ;; Insert ID links (nodes only)
	  (unless journal
	    (cl-loop for link in links
		     do (gnosis-nodes--insert-into
			 'node-links
			 `[,(cdr link) ,(car link)] t)))))
    (file-error
     (message "File error updating %s: %s.  \
Try M-x gnosis-nodes-db-force-sync to rebuild database."
              file (error-message-string err)))
    (error
     (message "Error updating %s: %s.  \
Try M-x gnosis-nodes-db-force-sync if issue persists."
              file (error-message-string err)))))

(defun gnosis-nodes--delete-file (&optional file)
  "Delete contents for FILE in database.
Removes node rows, associated links, and tags."
  (let* ((file (or file (file-name-nondirectory (buffer-file-name))))
	 (filename (file-name-nondirectory file))
	 (journal-p (file-in-directory-p file (gnosis-journal--dir)))
	 (nodes (if journal-p
		    (gnosis-nodes-select 'id 'journal `(= file ,filename) t)
		  (gnosis-nodes-select 'id 'nodes `(= file ,filename) t))))
    (gnosis-sqlite-with-transaction (gnosis--ensure-db)
      (cl-loop for node in nodes
	       do (progn
		    (if journal-p
			(gnosis-nodes--delete 'journal `(= id ,node))
		      (gnosis-nodes--delete 'nodes `(= id ,node)))
		    (gnosis-nodes--delete 'node-tag `(= node-id ,node))
		    (gnosis-nodes--delete 'node-links `(= source ,node))
		    (gnosis-nodes--delete 'node-links `(= dest ,node)))))))

(defun gnosis-nodes-update-file (&optional file)
  "Update contents of FILE in database.
Removes all contents of FILE in database, adding them anew.
When FILE is the current buffer's file, parses the buffer directly
instead of re-reading from disk (avoids re-decrypting .gpg files)."
  (let* ((file (or file (buffer-file-name)))
	 (journal-p (file-in-directory-p file (gnosis-journal--dir)))
	 (buf (and file (get-file-buffer file))))
    (gnosis-nodes--delete-file file)
    (gnosis-nodes--update-file file journal-p buf)
    ;; Update todos
    (when (and journal-p file)
      (gnosis-journal--update-todos file))))

;;;###autoload
(defun gnosis-nodes-delete-file (&optional file)
  "Delete FILE.
Delete file contents in database & file."
  (interactive)
  (let ((file (or file (file-name-nondirectory (buffer-file-name)))))
    (if (or (file-in-directory-p (buffer-file-name) gnosis-nodes-dir)
	    (file-in-directory-p (buffer-file-name) (gnosis-journal--dir)))
	(progn
	  (when (y-or-n-p (format "Delete file: %s?" file))
	    (gnosis-nodes--delete-file file)
	    (delete-file (buffer-file-name))
	    (kill-buffer (buffer-name))))
      (error "%s is not a gnosis node file" file))))

;;; Find/create operations

(defun gnosis-nodes-find--tag-with-tag-prop (lst)
  "Combine each sublist of strings in LST into a single string."
  (mapcar (lambda (item)
            (let* ((title (car item))
                   (tags (cadr item))
                   (propertized-tags
		    (when tags
		      (let ((tag-list (if (stringp tags) (read tags) tags)))
			(when (and tag-list (not (equal tag-list '())))
                          (concat
                           (propertize "#" 'face 'gnosis-nodes-face-tags)
                           (propertize
                            (mapconcat #'identity tag-list "#")
                            'face 'gnosis-nodes-face-tags)))))))
              (if propertized-tags
                  (format "%s  %s" title propertized-tags)
		title)))
          lst))

(cl-defun gnosis-nodes--create-file (title &optional directory extras)
  "Create a node FILE for TITLE.
Insert initial Org metadata if the buffer is new or empty.
DIRECTORY: Directory where the file is created.
EXTRAS: The template to be inserted at the start."
  (let* ((file (expand-file-name
		(gnosis-org--create-name
		 title nil
		 (and (eq directory (gnosis-journal--dir))
		      (bound-and-true-p gnosis-journal-as-gpg))
		 gnosis-nodes-create-as-gpg
		 gnosis-nodes-timestring)
		(or directory gnosis-nodes-dir)))
	 (org-id-track-globally nil))
    (when (and (file-exists-p file)
	       (not gnosis-nodes-timestring))
      (if (y-or-n-p (format "Node file already exists: %s.  Visit it?"
			    (file-name-nondirectory file)))
	  (progn (pop-to-buffer (find-file-noselect file))
		 (cl-return-from gnosis-nodes--create-file file))
	(user-error "Aborted: duplicate filename")))
    (let ((buffer (find-file-noselect file)))
      (with-current-buffer buffer
	(unless (or (file-exists-p file)
		    (> (buffer-size) 0))
	  (insert (format "#+title: %s\n#+filetags: \n" title))
	  (org-mode)
	  (org-id-get-create)
	  (when extras
	    (insert (gnosis-org-expand-headings extras)))))
      (switch-to-buffer buffer)
      (gnosis-nodes-mode 1))))

(defun gnosis-nodes-find--with-tags (&optional prompt entries)
  "Select gnosis node with tags from ENTRIES.
PROMPT: Prompt message."
  (replace-regexp-in-string
   "  #[^[:space:]]+" ""
   (funcall gnosis-nodes-completing-read-func
	    (or prompt "Select gnosis node: ")
	    (gnosis-nodes-find--tag-with-tag-prop
	     (or entries
		 (gnosis-nodes-select '[title tags] 'nodes))))))

(defun gnosis-nodes--find (prompt entries-with-tags entries)
  "PROMPT user to select from ENTRIES.
If `gnosis-nodes-show-tags' is non-nil, ENTRIES-WITH-TAGS will be used
instead."
  (let* ((entry (if gnosis-nodes-show-tags
                    (gnosis-nodes-find--with-tags
                     prompt entries-with-tags)
                  (funcall gnosis-nodes-completing-read-func
                           prompt entries))))
    entry))

;;;###autoload
(defun gnosis-nodes-find (&optional title file id directory templates)
  "Select gnosis node.
If there is no ID for TITLE, create a new FILE with TITLE as TOPIC in
DIRECTORY."
  (interactive)
  (gnosis-nodes-ensure-directories)
  (let* ((title (or title (if gnosis-nodes-show-tags
			      (gnosis-nodes-find--with-tags)
			    (funcall gnosis-nodes-completing-read-func
				     "Select gnosis node: "
				     (gnosis-nodes-select 'title 'nodes)))))
	 (file (or file (caar (gnosis-nodes-select 'file 'nodes `(= title ,title)))))
	 (id (or id (caar (gnosis-nodes-select 'id 'nodes `(= title ,title)))))
	 (directory (or directory gnosis-nodes-dir))
	 (templates (or templates gnosis-nodes-templates)))
    (cond ((null file)
	   (gnosis-nodes--create-file title directory
				      (gnosis-nodes-select-template templates)))
	  ((file-exists-p (expand-file-name file directory))
	   (gnosis-nodes-goto-id id))
	  (t (error "File %s does not exist.  \
Try `gnosis-nodes-db-force-sync' to resolve this"
		    file)))))

(defun gnosis-nodes--nodes-by-tag (tag)
  "Return all node IDs associated with TAG.
Uses the node-tag junction table for proper querying."
  (gnosis-nodes-select 'node-id 'node-tag `(= tag ,tag) t))

;;;###autoload
(defun gnosis-nodes-find-by-tag (&optional tag)
  "Find node under TAG."
  (interactive)
  (let* ((tag (or tag (funcall gnosis-nodes-completing-read-func
			       "Select tag: "
			       (gnosis-nodes--all-tags))))
	 (nodes-ids (gnosis-nodes--nodes-by-tag tag))
	 (node-titles (gnosis-nodes-select
		       'title 'nodes
		       `(in id ,(vconcat nodes-ids)) t))
	 (node (completing-read "Select node: " node-titles nil t)))
    (gnosis-nodes-find node)))

(defun gnosis-nodes--journal-buffer-p ()
  "Return non-nil if current buffer is a journal file."
  (and buffer-file-name
       (or (file-in-directory-p buffer-file-name (gnosis-journal--dir))
           (and gnosis-journal-file
                (string= (expand-file-name buffer-file-name)
                         (expand-file-name gnosis-journal-file))))))

(defun gnosis-nodes-select-template (&optional templates)
  "Select and evaluate a template from TEMPLATES.
When TEMPLATES is nil, detect whether the current buffer is a
journal file and use `gnosis-journal-templates' or
`gnosis-nodes-templates' accordingly.

Template functions may use \"{*}\" as a heading placeholder.
The caller expands markers via `gnosis-org-expand-headings'.
\"{*}\" becomes the base level, \"{**}\" one level deeper, etc."
  (let* ((templates (or templates
                        (if (gnosis-nodes--journal-buffer-p)
                            gnosis-journal-templates
                          gnosis-nodes-templates)))
         (template (if (= (length templates) 1)
                       (cdar templates)
                     (cdr (assoc
			   (funcall
			    gnosis-nodes-completing-read-func
			    "Select template: "
			    (mapcar #'car templates))
                           templates)))))
    (unless (functionp template)
      (user-error
       "Template is not a valid function; \
check `gnosis-nodes-templates'"))
    (funcall template)))

;;;###autoload
(defun gnosis-nodes-insert-template ()
  "Insert a template at point with context-aware headings.
Detects whether the current buffer is a journal file and uses
the appropriate template list."
  (interactive)
  (insert (gnosis-org-expand-headings (gnosis-nodes-select-template))))

;;;###autoload
(defun gnosis-nodes-insert (arg &optional journal-p)
  "Insert gnosis node link.
If called with ARG, prompt for custom link description.
If JOURNAL-P is non-nil, retrieve/create node as a journal entry."
  (interactive "P")
  (let* ((table (if journal-p 'journal 'nodes))
         (node (gnosis-nodes--find "Select gnosis node: "
                                   (gnosis-nodes-select '[title tags] table)
                                   (gnosis-nodes-select 'title table)))
         (id (car (gnosis-nodes-select 'id table `(= title ,node) t)))
	 (title (car (last (split-string node ":"))))
         (desc (cond ((use-region-p)
                      (buffer-substring-no-properties
                       (region-beginning) (region-end)))
                     (arg (read-string "Description: "))
                     (t title))))
    (unless id
      (save-window-excursion
        (gnosis-nodes--create-file
         node (if journal-p
                  (gnosis-journal--dir)
                gnosis-nodes-dir))
        (save-buffer)
        (setf id (car (gnosis-nodes-select 'id table `(= title ,node) t)))))
    (org-insert-link nil (format "id:%s" id) desc)
    (unless id (message "Created new node: %s" node))))

(defun gnosis-nodes--last-keyword-pos ()
  "Return end-of-line position of the last #+KEYWORD line at buffer top.
Returns nil if no keyword lines exist."
  (save-excursion
    (goto-char (point-min))
    (let (last-pos)
      (while (looking-at "^#\\+")
        (setq last-pos (line-end-position))
        (forward-line 1))
      last-pos)))

(defun gnosis-nodes--filetags ()
  "Return list of current filetags, or nil."
  (let ((case-fold-search t))
    (save-excursion
      (goto-char (point-min))
      (when (re-search-forward "^#\\+filetags:[ \t]*\\(.*\\)" nil t)
        (split-string (match-string 1) ":" t)))))

(defun gnosis-nodes--write-filetags (tags)
  "Write TAGS as a #+filetags line, replacing or creating it."
  (let ((case-fold-search t)
        (value (format " :%s:" (mapconcat #'identity tags ":"))))
    (save-excursion
      (goto-char (point-min))
      (cond
       ((re-search-forward "^#\\+filetags:" nil t)
        (delete-region (point) (line-end-position))
        (insert value))
       ((gnosis-nodes--last-keyword-pos)
        (goto-char (gnosis-nodes--last-keyword-pos))
        (end-of-line)
        (insert "\n#+filetags:" value))
       (t
        (insert "#+filetags:" value "\n"))))))

(defun gnosis-nodes-insert-filetag (&optional tag)
  "Insert TAG as filetag.
At a heading, add TAG to heading tags.  Otherwise, add to #+FILETAGS."
  (interactive)
  (let ((tag (or tag (funcall gnosis-nodes-completing-read-func
                              "Select tag: " (gnosis-nodes--all-tags)))))
    (if (org-at-heading-p)
        (org-set-tags
         (cl-union (list tag) (org-get-tags nil t)
                   :test #'string=))
      (let ((existing (gnosis-nodes--filetags)))
        (unless (member tag existing)
          (gnosis-nodes--write-filetags (append existing (list tag))))))))

;;;###autoload
(defun gnosis-nodes-insert-tags (tags)
  "Insert TAGS as filetags."
  (interactive
   (list (completing-read-multiple
	  "Select tags (separated by ,): "
	  (gnosis-nodes--all-tags))))
  (let ((id (gnosis-org-get-id))
	(org-id-track-globally nil))
    (org-id-goto id)
    (if (org-current-level)
	(org-set-tags (cl-union tags (org-get-tags nil t) :test #'string=))
      (dolist (tag tags)
	(gnosis-nodes-insert-filetag tag)))))

;;;###autoload
(defun gnosis-nodes-visit-backlinks ()
  "Visit backlinks for current node."
  (interactive)
  (let* ((id (gnosis-org-get-id))
	 (source-ids (gnosis-nodes-select 'source 'node-links `(= dest ,id) t))
	 (titles (when source-ids
		   (mapcar #'car
			   (gnosis-sqlite-select-batch (gnosis--ensure-db)
						       "SELECT title FROM nodes WHERE id IN (%s)"
						       source-ids)))))
    (if titles
	(gnosis-nodes-find
	 (completing-read "Backlink: " titles))
      (message "No backlinks found for current node"))))

(defun gnosis-nodes-get-nodes-data (&optional node-ids)
  "Fetch node data for NODE-IDS or all nodes if not specified.
Returns a list of (ID TITLE BACKLINK-COUNT) for each node."
  (let* ((nodes (if node-ids
                    (gnosis-nodes-select '[id title] 'nodes
                                         `(in id ,(vconcat node-ids)))
                  (gnosis-nodes-select '[id title] 'nodes)))
         (backlinks (if node-ids
                        (gnosis-nodes-select '[dest source] 'node-links
                                             `(in dest ,(vconcat node-ids)))
                      (gnosis-nodes-select '[dest source] 'node-links)))
         (backlinks-count-hash (let ((hash (make-hash-table :test 'equal)))
                                 (dolist (link backlinks hash)
                                   (let ((dest (nth 0 link)))
                                     (puthash dest
                                              (1+ (or (gethash dest hash) 0))
                                              hash))))))
    (mapcar (lambda (node)
              (let ((id (nth 0 node))
                    (title (nth 1 node)))
                (list id title (or (gethash id backlinks-count-hash) 0))))
            nodes)))

;;; Navigation

(defun gnosis-nodes--get-id-at-point ()
  "Return the Org ID link at point, if any."
  (let* ((element (org-element-context))
         (id-link
          (when (and (eq (org-element-type element) 'link)
                     (string= (org-element-property :type element)
                              "id"))
            (org-element-property :path element))))
    id-link))

(defun gnosis-nodes-goto-id (&optional id)
  "Visit file for ID.
If file or id are not found, use `org-open-at-point'."
  (interactive)
  (let* ((id (or id (gnosis-nodes--get-id-at-point)))
	 (org-id-track-globally nil))
    (cond ((gnosis-nodes-select 'file 'nodes `(= id ,id))
	   (find-file
	    (expand-file-name (car (gnosis-nodes-select 'file 'nodes `(= id ,id) t))
			      gnosis-nodes-dir))
	   (org-id-goto id))
	  ((gnosis-nodes-select 'file 'journal `(= id ,id))
	   (find-file
	    (expand-file-name
	     (car (gnosis-nodes-select 'file 'journal
				       `(= id ,id) t))
	     (gnosis-journal--dir)))
	   (org-id-goto id))
	  (t (org-open-at-point)))
    (gnosis-nodes-mode 1)))

;;; Sync

(defun gnosis-nodes--file-changed-p (file table)
  "Check if FILE changed since last sync using mtime then hash.
TABLE is either \\='nodes or \\='journal."
  (let* ((filename (file-name-nondirectory file))
         (file-mtime
          (format-time-string
           "%s" (file-attribute-modification-time
                 (file-attributes file))))
         (db-data (car (gnosis-nodes-select
                        '[mtime hash] table
                        `(= file ,filename))))
         (db-mtime (car db-data))
         (db-hash (cadr db-data)))
    (or (not db-mtime)
        (and (not (string= file-mtime db-mtime))
             (not (string= (gnosis-org--file-hash file) db-hash))))))

(defun gnosis-nodes-db-update-files (&optional force)
  "Sync node files with progress reporting.
When FORCE, update all files.  Otherwise, only update changed files."
  (gnosis-nodes-ensure-directories)
  (let* ((all-files (cl-remove-if-not
                     (lambda (file)
                       (and (string-match-p "\\.org\\(?:\\.gpg\\)?$" file)
                            (not (file-directory-p file))))
                     (directory-files gnosis-nodes-dir t nil t)))
         (files (if force
                    all-files
                  (cl-remove-if-not
                   (lambda (file) (gnosis-nodes--file-changed-p file 'nodes))
                   all-files))))
    (if (zerop (length files))
        (message "No files to sync")
      (let ((progress (make-progress-reporter
                       (format "Processing %d/%d files..."
                               (length files)
                               (length all-files))
                       0 (length files))))
        (cl-loop for file in files
                 for i from 0
                 do (progn
                      (gnosis-nodes-update-file file)
                      (progress-reporter-update progress i)))
        (progress-reporter-done progress)))))

(defun gnosis-nodes--purge-tables ()
  "Delete all rows from node and journal tables for full rebuild."
  (dolist (table '(nodes journal node-links node-tag))
    (gnosis--delete table)))

;;;###autoload
(defun gnosis-nodes-db-sync (&optional force)
  "Sync node database with progress reporting.
When FORCE (prefix arg), rebuild from scratch."
  (interactive "P")
  (let ((gc-cons-threshold most-positive-fixnum))
    (gnosis-nodes-ensure-directories)
    (message "Syncing nodes database...")
    (gnosis-sqlite-with-transaction (gnosis--ensure-db)
      (when force
	(gnosis-nodes--purge-tables)
	(message "Purged all node/journal tables for rebuild."))
      (gnosis-nodes-db-update-files force))
    ;; Sync journal files
    (message "Syncing journal files...")
    (gnosis-journal-db-sync force)
    (message "Node sync complete!")))

;;;###autoload
(defun gnosis-nodes-db-force-sync ()
  "Force rebuild node data from files."
  (interactive)
  (when (y-or-n-p "Force rebuild node database from files?")
    (gnosis-nodes-db-sync 'force)))

;;; Minor mode

(defvar-keymap gnosis-nodes-mode-map
  :doc "gnosis-nodes keymap"
  "C-c C-o" #'gnosis-nodes-goto-id
  "C-c C-q" #'gnosis-nodes-insert-tags)

(define-minor-mode gnosis-nodes-mode
  "Gnosis nodes mode."
  :lighter " gnosis-nodes"
  :keymap gnosis-nodes-mode-map
  :global nil
  :group 'gnosis-nodes
  (if gnosis-nodes-mode
      (add-hook 'after-save-hook #'gnosis-nodes-update-file nil t)
    (remove-hook 'after-save-hook #'gnosis-nodes-update-file t)))

(defun gnosis-nodes--find-file-h ()
  "Enable `gnosis-nodes-mode' for org files in gnosis directories.
Added to `org-mode-hook'."
  (when (and buffer-file-name
             (derived-mode-p 'org-mode)
             (or (file-in-directory-p
                  buffer-file-name gnosis-nodes-dir)
                 (file-in-directory-p
                  buffer-file-name (gnosis-journal--dir))))
    (gnosis-nodes-mode 1)))

(add-hook 'org-mode-hook #'gnosis-nodes--find-file-h)

;; Forward declarations for journal functions
(declare-function gnosis-journal-db-sync "gnosis-journal")
(declare-function gnosis-journal--update-todos "gnosis-journal")

(provide 'gnosis-nodes)
;;; gnosis-nodes.el ends here
