;;; org-gtd-refile.el --- refiling logic for org gtd -*- lexical-binding: t; coding: utf-8 -*-
;;
;; Copyright © 2019-2023, 2025 Aldric Giacomoni

;; Author: Aldric Giacomoni <trevoke@gmail.com>
;; This file is not part of GNU Emacs.

;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.

;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this file.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; Refiling logic for org-gtd.
;;
;;; Code:

;;;; Requirements

(require 'org)
(require 'org-refile)
(require 'org-element)

(require 'org-gtd-core)
(require 'org-gtd-files)
(require 'org-gtd-types)

;;;; Customization

(defcustom org-gtd-refile-to-any-target t
  "Set to true if you do not need to choose where to refile processed items.

When this is true, org-gtd will refile to the first target it finds, or creates
it if necessary, without confirmation.  When this is false, it will ask for
confirmation regardless of the number of options.  Note that setting this to
false does not mean you can safely create new targets.  See the documentation
to create new refile targets.

Defaults to true to carry over pre-2.0 behavior.  You will need to change this
setting as part of following the instructions to add your own refile targets."
  :group 'org-gtd-organize
  :package-version '(org-gtd . "2.0.0")
  :type 'boolean)

(make-obsolete-variable 'org-gtd-refile-to-any-target
                        'org-gtd-refile-prompt-for-types
                        "4.0.0")

(defcustom org-gtd-refile-prompt-for-types
  '(single-action project-heading project-task calendar someday delegated tickler habit)
  "Obsolete since 4.1.0.  List of GTD types that should prompt for refile.

The contents of this list are migrated once, at load time, into per-type
`:prompt-to-refile t' entries in the `org-gtd-types' registry.  Further
mutations of this variable are not honored -- setting it after load will
have no effect on refile behavior.

To configure prompting behavior now, use `org-gtd-customize-type' to set
`:prompt-to-refile' on individual types, or set `org-gtd-refile-prompt-default'
as a global fallback for types that do not declare the property explicitly."
  :group 'org-gtd-organize
  :package-version '(org-gtd . "4.0.0")
  :type '(repeat symbol))

(make-obsolete-variable 'org-gtd-refile-prompt-for-types
                        "use per-type :prompt-to-refile or `org-gtd-refile-prompt-default'"
                        "4.1.0")

;; Load-time migration: copy the legacy list into per-type
;; `:prompt-to-refile t' entries in the type registry.  Unknown types
;; (e.g. `single-action', `project-heading', `project-task') are
;; silently skipped -- Phase 5 will move these declarations into the
;; per-type module files, which may not have been loaded yet when this
;; form runs.  This migration is one-shot at load time; mutating
;; `org-gtd-refile-prompt-for-types' afterward has no effect.
(dolist (type (with-no-warnings org-gtd-refile-prompt-for-types))
  (when (assq type org-gtd-types)
    (org-gtd-customize-type type :prompt-to-refile t)))

(defcustom org-gtd-refile-prompt-default nil
  "Fallback value for whether refile should prompt for a target.

Used by `org-gtd-refile--should-prompt-p' when the item's GTD type does
not declare `:prompt-to-refile' explicitly in the type registry."
  :group 'org-gtd-organize
  :package-version '(org-gtd . "4.1.0")
  :type 'boolean)

;;;; Variables

(defvar org-gtd--organize-type)

(defvar org-gtd-refile--deprecated-warning-shown nil
  "Non-nil if deprecation warning for `org-gtd-refile-to-any-target' was shown.")


;;;; Functions

;;;;; Helper

(defun org-gtd-refile--wip-file-p (file)
  "Return non-nil if FILE is in the WIP temp directory.
WIP temp files are used during clarification and should never be
valid refile targets."
  (when file
    (let ((wip-temp-dir (expand-file-name "org-gtd" temporary-file-directory)))
      (string-prefix-p (file-name-as-directory wip-temp-dir)
                       (expand-file-name file)))))

(defun org-gtd-refile--make-verify-function (type)
  "Create a verify function for refile targets of TYPE.
Returns a function suitable for `org-refile-target-verify-function'.

The function filters targets as follows:
- WIP temp files: always rejected (prevents self-refile errors)
- Inbox file: always rejected (inbox is for capture, not refile)
- Files in `org-gtd-directory': must have matching ORG_GTD_REFILE property
- Other files: accepted (allows user's custom refile targets)"
  (lambda ()
    (let* ((file (buffer-file-name))
           (gtd-dir (expand-file-name org-gtd-directory))
           (inbox-path (expand-file-name (org-gtd--path "inbox")))
           (in-wip-dir (org-gtd-refile--wip-file-p file))
           (is-inbox (and file (string-equal (expand-file-name file) inbox-path)))
           (in-gtd-dir (and file (string-prefix-p gtd-dir (expand-file-name file))))
           (refile-prop (org-element-property :ORG_GTD_REFILE (org-element-at-point))))
      (cond
       ;; WIP temp files should never be refile targets
       (in-wip-dir nil)
       ;; Inbox file should never be a refile target
       (is-inbox nil)
       ;; Files in GTD dir must have matching ORG_GTD_REFILE property (multivalue)
       (in-gtd-dir (and refile-prop
                        (member type (split-string refile-prop))))
       ;; Other files (user's custom targets) are accepted
       (t t)))))

(defun org-gtd-refile--should-prompt-p (type)
  "Return non-nil if refiling TYPE should prompt for target selection.

Precedence:
1. If `org-gtd-refile-to-any-target' is non-nil, emit the deprecation
   warning and return nil (legacy behavior).
2. Otherwise, if the type registry declares `:prompt-to-refile' for
   TYPE (even explicitly nil), return that value.
3. Otherwise, fall back to `org-gtd-refile-prompt-default'."
  (cond
   (org-gtd-refile-to-any-target
    (unless org-gtd-refile--deprecated-warning-shown
      (display-warning 'org-gtd
                       "`org-gtd-refile-to-any-target' is deprecated as of 4.0.0.
Set it to nil and declare :prompt-to-refile on the relevant types instead.")
      (setq org-gtd-refile--deprecated-warning-shown t))
    nil)
   ((org-gtd-type-prompt-to-refile-set-p type)
    (org-gtd-type-prompt-to-refile type))
   (t org-gtd-refile-prompt-default)))

;;;;; Private

(defun org-gtd-refile--do (type refile-target-element)
  "Refile an item to an appropriate GTD location.

TYPE is one of the org-gtd action types (e.g., `org-gtd-projects').
REFILE-TARGET-ELEMENT is a string template for creating a new target if needed.

When prompting (type in `org-gtd-refile-prompt-for-types'), shows merged
targets from user's `org-refile-targets' and GTD files.

When auto-refiling (type not in prompt list), only uses GTD targets in
`org-gtd-tasks.org', ignoring user configuration."
  ;; Check GTD-only targets for existence (not merged targets)
  (unless (org-gtd-refile--get-gtd-targets type)
    (org-gtd-refile--add-target refile-target-element))

  (if (org-gtd-refile--should-prompt-p org-gtd--organize-type)
      ;; Prompt mode: use merged targets (user + GTD)
      (let ((org-refile-target-verify-function (org-gtd-refile--make-verify-function type))
            (org-refile-targets (append org-refile-targets
                                        '((org-agenda-files :maxlevel . 9))))
            (org-refile-use-outline-path t)
            (org-outline-path-complete-in-steps nil))
        (org-refile nil nil nil "Finish organizing task under: "))
    ;; Auto-refile: GTD-only targets
    (org-refile nil nil (car (org-gtd-refile--get-gtd-targets type)))))

(defun org-gtd-refile--get-targets (type)
  "Get refile targets for TYPE, merging user's targets with org-gtd's.

Returns the list of refile targets that would be available when refiling
an item of TYPE (e.g., `org-gtd-projects', variable `org-gtd-tickler')."
  (let ((org-refile-target-verify-function (org-gtd-refile--make-verify-function type))
        (org-refile-targets (append org-refile-targets
                                    '((org-agenda-files :maxlevel . 9))))
        (org-refile-use-outline-path t)
        (org-outline-path-complete-in-steps nil))
    (org-refile-get-targets)))

(defun org-gtd-refile--get-gtd-targets (type)
  "Get GTD-only refile targets for TYPE.
Only looks in `org-gtd-tasks.org', ignoring user's `org-refile-targets'
and other `org-agenda-files'.  This ensures auto-refile always goes to
the GTD file, regardless of user configuration."
  (let* ((gtd-file (org-gtd--path org-gtd-default-file-name))
         (org-refile-target-verify-function (org-gtd-refile--make-verify-function type))
         (org-refile-targets `(((,gtd-file) :maxlevel . 9)))
         (org-refile-use-outline-path t)
         (org-outline-path-complete-in-steps nil))
    (org-refile-get-targets)))

(defun org-gtd-refile--add-target (refile-target-element)
  "Create a missing org-gtd refile target in the default GTD file.

REFILE-TARGET-ELEMENT is a string version of a valid org-heading target.

Creates the target in `org-gtd--default-file' to ensure consistent location.
In future, this could be enhanced to prompt for file location when multiple
org-gtd files exist."
  (with-current-buffer (org-gtd--default-file)
    ;; Preserve point: when the item being organized lives in this same
    ;; buffer (standalone re-organize), `with-current-buffer' does not
    ;; restore point, so moving to `point-max' would leave the subsequent
    ;; `org-refile' acting on the wrong subtree (issue #288).
    (save-excursion
      (goto-char (point-max))
      (newline)
      (insert refile-target-element)
      (basic-save-buffer))))

(defun org-gtd-refile--group-p (type)
  "Determine whether the current heading is of a given gtd TYPE.
TYPE can be one of multiple space-separated values in ORG_GTD_REFILE."
  (let ((refile-prop (org-element-property :ORG_GTD_REFILE (org-element-at-point))))
    (and refile-prop
         (member type (split-string refile-prop)))))

;;;; Footer

(provide 'org-gtd-refile)

;;; org-gtd-refile.el ends here
