;;; my-org-jira.el -- Extensions for org-jira -*- lexical-binding: t -*-

;; Copyright (C) 2023 Free Software Foundation.

;; Author: Yuchen Pei <id@ypei.org>
;; Package-Requires: ((emacs "28.2"))

;; This file is part of dotted.

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

;; dotted 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 Affero General
;; Public License for more details.

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

;;; Commentary:

;; Extensions for org-jira.

;;; Code:

(require 'org-jira)

;;; override `org-jira-sdk-issue'
(defclass org-jira-sdk-issue (org-jira-sdk-record)
  ((affected-versions :type string :initarg :affected-versions)
   (assignee :type (or null string) :initarg :assignee)
   (components :type string :initarg :components)
   (fix-versions :type string :initarg :fix-versions)
   (labels :type string :initarg :labels)
   (created :type string :initarg :created)
   (description :type (or null string) :initarg :description)
   (duedate :type (or null string) :initarg :duedate)
   (headline :type string :initarg :headline)
   (id :type string :initarg :id)       ; TODO: Probably remove me
   (issue-id :type string :initarg :issue-id :documentation "The common ID/key, such as EX-1.")
   (issue-id-int :type string :initarg :issue-id-int :documentation "The internal Jira ID, such as 12345.")
   (filename :type (or null string) :initarg :filename :documentation "The filename to write issue to.")
   (priority :type (or null string) :initarg :priority)
   (proj-key :type string :initarg :proj-key)
   (related-issues :type string :initarg :related-issues)
   (reporter :type (or null string) :initarg :reporter)
   (resolution :type (or null string) :initarg :resolution)
   (sprint :type (or null string) :initarg :sprint)
   (start-date :type (or null string) :initarg :start-date)
   (status :type string :initarg :status)
   (summary :type string :initarg :summary)
   (type :type string :initarg :type)
   (type-id :type string :initarg :type-id)
   (updated :type string :initarg :updated)
   (data :initarg :data :documentation "The remote Jira data object (alist).")
   (hydrate-fn :initform #'jiralib-get-issue :initarg :hydrate-fn))
  "An issue on the end.  ID of the form EX-1, or a numeric such as 10000.")


;;; override `org-jira-sdk-from-data'
(cl-defmethod org-jira-sdk-from-data ((rec org-jira-sdk-issue))
  ;; (print rec)
  (cl-flet ((path (keys) (org-jira-sdk-path (oref rec data) keys)))
    (org-jira-sdk-issue
     :affected-versions (mapconcat (lambda (c) (org-jira-sdk-path c '(name))) (path '(fields versions)) ", ")
     :assignee (path '(fields assignee displayName))
     :components (mapconcat (lambda (c) (org-jira-sdk-path c '(name))) (path '(fields components)) ", ")
     :fix-versions (mapconcat (lambda (c) (org-jira-sdk-path c '(name))) (path '(fields fixVersions)) ", ")
     :labels (mapconcat (lambda (c) (format "%s" c)) (mapcar #'identity (path '(fields labels))) ", ")
     :created (path '(fields created))     ; confirm
     :description (or (path '(fields description)) "")
     :duedate (or (path '(fields sprint endDate)) (path '(fields duedate)))         ; confirm
     :filename (path '(fields project key))
     :headline (path '(fields summary)) ; Duplicate of summary, maybe different.
     :id (path '(key))
     :issue-id (path '(key))
     :issue-id-int (path '(id))
     :priority (path '(fields priority name))
     :proj-key (path '(fields project key))
     :related-issues (mapconcat
                      (lambda (c)
                        ;; (print c)
                        (if (org-jira-sdk-path c '(inwardIssue))
                            (if (equal
                                 (org-jira-sdk-path
                                  c '(inwardIssue fields status name))
                                 "Closed")
                                ""
                              (format "%s: %s %s"
                                      (org-jira-sdk-path c '(type inward))
                                      (org-jira-sdk-path c '(inwardIssue key))
                                      (org-jira-sdk-path c '(inwardIssue fields summary))))
                          (if (equal
                               (org-jira-sdk-path
                                c '(outwardIssue fields status name))
                               "Closed")
                              ""
                            (format "%s: %s %s"
                                    (org-jira-sdk-path c '(type outward))
                                    (org-jira-sdk-path c '(outwardIssue key))
                                    (org-jira-sdk-path c '(outwardIssue fields summary))))))
                      (path '(fields issuelinks)) "; ")
     :reporter (path '(fields reporter displayName)) ; reporter could be an object of its own slot values
     :resolution (path '(fields resolution name))  ; confirm
     :sprint (path '(fields sprint name))
     :start-date (path '(fields start-date))  ; confirm
     :status (org-jira-decode (path '(fields status name)))
     :summary (path '(fields summary))
     :type (path '(fields issuetype name))
     :type-id (path '(fields issuetype id))
     :updated (path '(fields updated))  ; confirm
     ;; TODO: Remove this
     ;; :data (oref rec data)
     )))

;; Override `org-jira--render-issue'
;; include issue-id in the headline
(defun my-org-jira--render-issue (Issue)
  "Render single ISSUE."
  ;;  (org-jira-log "Rendering issue from issue list")
  ;;  (org-jira-log (org-jira-sdk-dump Issue))
  ;; (print Issue)
  (with-slots (filename proj-key issue-id summary status priority headline id) Issue
    (let (p)
      (with-current-buffer (org-jira--get-project-buffer Issue)
        (org-jira-freeze-ui
          (org-jira-maybe-activate-mode)
          (org-jira--maybe-render-top-heading proj-key)
          (setq p (org-find-entry-with-id issue-id))
          (save-restriction
            (if (and p (>= p (point-min))
                     (<= p (point-max)))
                (progn
                  (goto-char p)
                  (forward-thing 'whitespace)
                  (org-jira-kill-line))
              (goto-char (point-max))
              (unless (looking-at "^")
                (insert "\n"))
              (insert "** "))
            (org-jira-insert
             (concat (org-jira-get-org-keyword-from-status status)
                     " "
                     (org-jira-get-org-priority-cookie-from-issue priority)
                     issue-id " " headline))
            (save-excursion
              (unless (search-forward "\n" (point-max) 1)
                (insert "\n")))
            (org-narrow-to-subtree)
            (save-excursion
              (org-back-to-heading t)
              (org-set-tags-to (replace-regexp-in-string "-" "_" issue-id)))
            (mapc (lambda (entry)
                    (let ((val (slot-value Issue entry)))
                      (when (or (and val (not (string= val "")))
                                (eq entry 'assignee)) ;; Always show assignee
                        (org-jira-entry-put (point) (symbol-name entry) val))))
                  '(assignee filename reporter type type-id priority affected-versions fix-versions labels resolution status components created updated sprint related-issues))

            (org-jira-entry-put (point) "ID" issue-id)
            (org-jira-entry-put (point) "CUSTOM_ID" issue-id)

            ;; Insert the duedate as a deadline if it exists
            (when org-jira-deadline-duedate-sync-p
              (let ((duedate (oref Issue duedate)))
                (when (> (length duedate) 0)
                  (org-deadline nil duedate))))

            (mapc
             (lambda (heading-entry)
               (ensure-on-issue-id-with-filename issue-id filename
                                                 (let* ((entry-heading
                                                         (concat (symbol-name heading-entry)
                                                                 (format ": [[%s][%s]]"
                                                                         (concat jiralib-url "/browse/" issue-id) issue-id))))
                                                   (setq p (org-find-exact-headline-in-buffer entry-heading))
                                                   (if (and p (>= p (point-min))
                                                            (<= p (point-max)))
                                                       (progn
                                                         (goto-char p)
                                                         (org-narrow-to-subtree)
                                                         (goto-char (point-min))
                                                         (forward-line 1)
                                                         (delete-region (point) (point-max)))
                                                     (if (org-goto-first-child)
                                                         (org-insert-heading)
                                                       (goto-char (point-max))
                                                       (org-insert-subheading t))
                                                     (org-jira-insert entry-heading "\n"))

                                                   ;;  Insert 2 spaces of indentation so Jira markup won't cause org-markup
                                                   (org-jira-insert
                                                    (replace-regexp-in-string
                                                     "^" "  "
                                                     (format "%s" (slot-value Issue heading-entry)))))))
             '(description))

            (when org-jira-download-comments
              (org-jira-update-comments-for-issue Issue)

              ;; FIXME: Re-enable when attachments are not erroring.
              ;;(org-jira-update-attachments-for-current-issue)
              )

            ;; only sync worklog clocks when the user sets it to be so.
            (when org-jira-worklog-sync-p
              (org-jira-update-worklogs-for-issue issue-id filename))))))))

;; Overload `org-jira-update-worklogs-from-org-clocks'.
(defun my-org-jira-update-worklogs-from-org-clocks ()
  "Update or add a worklog based on the org clocks."
  (interactive)
  (let* ((issue-id (org-jira-get-from-org 'issue 'key))
         (filename (org-jira-filename))
         ;; Fetch all workflogs for this issue
         (jira-worklogs-ht (org-jira-worklog-to-hashtable issue-id)))
    (org-jira-log (format "About to sync worklog for issue: %s in file: %s"
                          issue-id filename))
    (ensure-on-issue-id-with-filename
        issue-id filename
        (search-forward (format ":%s:" (or (org-clock-drawer-name) "LOGBOOK"))  nil 1 1)
        (org-beginning-of-line)
        ;; (org-cycle 1)
        (while (search-forward "CLOCK: " nil 1 1)
          (let ((org-time (buffer-substring-no-properties (point) (point-at-eol))))
            (forward-line)
            ;; See where the stuff ends (what point)
            (let (next-clock-point)
              (save-excursion
                (search-forward-regexp "\\(CLOCK\\|:END\\):" nil 1 1)
                (setq next-clock-point (point)))
              (let ((clock-content
                     (buffer-substring-no-properties (point) next-clock-point)))
                ;; Update via jiralib call
                (let* ((worklog (org-jira-org-clock-to-jira-worklog org-time clock-content))
                       (comment-text (cdr (assoc 'comment worklog)))
                       (comment-text (if (string= (org-trim comment-text) "") nil comment-text)))
                  (unless (cdr (assoc 'worklog-id worklog))
                    (jiralib-add-worklog
                     issue-id
                     (cdr (assoc 'started worklog))
                     (cdr (assoc 'time-spent-seconds worklog))
                     comment-text
                     nil) ; no callback - synchronous
                    )
                  )))))
        (org-jira-log (format "Updating worklog from org-jira-update-worklogs-from-org-clocks call"))
        (org-jira-update-worklogs-for-issue issue-id filename)
        )))

(defun my-org-jira-comment-url (issue-id comment-id)
  (format
   "%s/browse/%s?focusedCommentId=%s&page=com.atlassian.jira.plugin.system.issuetabpanels%%3Acomment-tabpanel#comment-%s"
   jiralib-url issue-id comment-id comment-id))

(defun my-org-jira-comment-url-at-point ()
  (my-org-jira-comment-url
   (org-entry-get
    (save-excursion
      (outline-up-heading 1)
      (point))
    "ID")
   (org-entry-get (point) "ID")))

(defun my-org-jira-kill-comment-url-at-point ()
  (interactive)
  (kill-new (my-org-jira-comment-url-at-point)))

(defun my-org-jira-url-p (url)
  (string-match-p (format "^%s/browse/[^/]" jiralib-url) url))

(defun my-org-jira-open-url (url)
  (interactive "sJIRA issue url: ")
  (when (string-match (format "^%s/browse/\\([^/]+\\)" jiralib-url) url)
    (org-jira-get-issue (match-string 1 url))))

(provide 'my-org-jira)
;;; my-org-jira.el ends here