(require 'seq)
(require 'subr-x)
;;makes sure elfeed reads index from disk before launching
(defvar fscotto/elfeed-initial-update-done nil
  "Non-nil once Elfeed has triggered its first automatic update this session.")

(defvar fscotto/elfeed-update-timer nil
  "Timer used for periodic Elfeed updates.")

(defun fscotto/elfeed-auto-update ()
  "Refresh Elfeed feeds in the background."
  (when (require 'elfeed nil 'noerror)
    (require 'elfeed-db)
    (elfeed-db-load)
    (elfeed-update)))

(defun fscotto/elfeed-start-auto-update ()
  "Start the periodic Elfeed update timer if needed."
  (unless (timerp fscotto/elfeed-update-timer)
    (setq fscotto/elfeed-update-timer
          (run-at-time "15 min" (* 15 60) #'fscotto/elfeed-auto-update))))

(defun fscotto/elfeed-load-db-and-open ()
  "Open Elfeed, updating once per session and then every 15 minutes.

Based on https://pragmaticemacs.wordpress.com/2016/08/17/read-your-rss-feeds-in-emacs-with-elfeed/
  Created: 2016-08-17
  Updated: 2025-06-13"
  (interactive)
  (require 'elfeed)
  (require 'elfeed-db)
  (elfeed-db-load)
  (elfeed)
  (fscotto/elfeed-start-auto-update)
  (unless fscotto/elfeed-initial-update-done
    (setq fscotto/elfeed-initial-update-done t)
    (message "Updating Elfeed feeds...")
    (elfeed-update)))

(defun fscotto/project-root ()
  "Return projectile project root or fallback to default-directory."
  (if (featurep 'projectile)
      (or (projectile-project-root) default-directory)
    default-directory))

(defun fscotto/project-vterm ()
  "Open vterm in project root."
  (interactive)
  (let ((default-directory (fscotto/project-root)))
    (vterm)))

(defun fscotto/open-multi-vterm-in (directory)
  "Open a new multi-vterm buffer in DIRECTORY."
  (let ((default-directory (file-name-as-directory directory)))
    (multi-vterm)))

(defun fscotto/home-multi-vterm ()
  "Open a new multi-vterm buffer in HOME."
  (interactive)
  (fscotto/open-multi-vterm-in (expand-file-name "~/")))

(defun fscotto/project-multi-vterm ()
  "Open a new multi-vterm buffer in project root."
  (interactive)
  (fscotto/open-multi-vterm-in (fscotto/project-root)))

(defconst fscotto/opencode-db-path
  (expand-file-name "~/.local/share/opencode/opencode.db")
  "Path to the OpenCode SQLite database.")

(defun fscotto/launch-external-terminal (&optional command directory)
  "Launch external terminal in DIRECTORY, optionally running COMMAND."
  (let* ((default-directory (file-name-as-directory (or directory (fscotto/project-root))))
         (terminal-program (or (executable-find fscotto/external-terminal-program)
                               fscotto/external-terminal-program))
         (args (append
                 (list fscotto/external-terminal-working-directory-option
                      default-directory)
                (when command
                  (append
                   (list fscotto/external-terminal-execute-option)
                   command)))))
    (unless (file-executable-p terminal-program)
      (user-error "External terminal not found: %s" fscotto/external-terminal-program))
    (apply #'start-process
           "fscotto-external-terminal"
           nil
           terminal-program
           args)))

(defun fscotto/opencode-session-candidates (directory)
  "Return OpenCode session candidates for DIRECTORY.

Each entry is a cons cell of display string and session id."
  (let ((sqlite3-program (executable-find "sqlite3"))
        (session-directory (directory-file-name (expand-file-name directory))))
    (unless sqlite3-program
      (user-error "sqlite3 not found"))
    (unless (file-exists-p fscotto/opencode-db-path)
      (user-error "OpenCode database not found: %s" fscotto/opencode-db-path))
    (mapcar
     (lambda (line)
       (pcase-let* ((fields (split-string line "\t"))
                    (`(,session-id ,title ,updated-at) fields)
                    (title (if (and title (not (string= title "")))
                               title
                             "Untitled session"))
                    (updated-seconds (/ (string-to-number updated-at) 1000))
                    (updated-label (format-time-string "%Y-%m-%d %H:%M"
                                                       (seconds-to-time updated-seconds)))
                    (short-id (if (> (length session-id) 12)
                                  (substring session-id 0 12)
                                session-id)))
         (cons (format "%s | %s | %s" title updated-label short-id)
               session-id)))
     (process-lines
      sqlite3-program
      "-tabs"
      "-noheader"
      fscotto/opencode-db-path
      (format (concat
               "select id, title, time_updated "
               "from session "
               "where directory = '%s' and time_archived is null "
               "order by time_updated desc;")
              (replace-regexp-in-string "'" "''" session-directory))))))

(defun fscotto/project-opencode-latest-session-id ()
  "Return the latest saved OpenCode session id for the current project."
  (cdr (car (fscotto/opencode-session-candidates (fscotto/project-root)))))

(defun fscotto/project-agent-dwim ()
  "Choose an agent for the current project and launch it externally."
  (interactive)
  (let ((agent (completing-read "Agent: " '("Claude" "Codex" "Gemini" "OpenCode") nil t)))
    (pcase agent
      ("Claude"
       (fscotto/launch-external-terminal '("claude" "--continue")))
      ("OpenCode"
       (let ((session-id (fscotto/project-opencode-latest-session-id)))
         (if session-id
             (fscotto/launch-external-terminal (list "opencode" "--session" session-id)
                                               (fscotto/project-root))
           (fscotto/project-opencode))))
      ("Codex"
       (fscotto/launch-external-terminal '("codex" "resume" "--last")))
      ("Gemini"
       (fscotto/launch-external-terminal '("gemini" "--resume" "latest")
                                         (fscotto/project-root))))))

(defun fscotto/project-opencode-session ()
  "Resume a saved OpenCode session for the current project."
  (interactive)
  (let* ((project-directory (fscotto/project-root))
         (candidates (fscotto/opencode-session-candidates project-directory)))
    (unless candidates
      (user-error "No OpenCode sessions found for %s" project-directory))
    (let* ((selection (completing-read "OpenCode session: " candidates nil t))
           (session-id (cdr (assoc selection candidates))))
      (fscotto/launch-external-terminal (list "opencode" "--session" session-id)
                                        project-directory))))

(defun fscotto/gemini-session-candidates (directory)
  "Return Gemini session candidates for DIRECTORY.

Each entry is a cons cell of display string and session index.
Tries JSON output first, falls back to text parsing if unavailable."
  (let* ((default-directory (file-name-as-directory directory))
         (json-output (shell-command-to-string
                       "gemini --list-sessions --output-format json 2>/dev/null")))
    (cond
     ((string-match "^{" json-output)
      (ignore-errors
        (require 'json)
        (let* ((parsed (json-parse-string json-output))
               (sessions (gethash "sessions" parsed)))
          (when (vectorp sessions)
            (seq-map-indexed
             (lambda (s idx)
               (let* ((idx-str (number-to-string (1+ idx)))
                      (msg (if (hash-table-p s)
                               (or (gethash "firstUserMessage" s) "Session")
                             "Session"))
                      (ts (and (hash-table-p s)
                               (ignore-errors (gethash "lastUpdated" s))
                               (when (stringp it) (string-trim it))))
                      (label (if ts (format "%s [%s]" msg ts) msg)))
                 (cons label idx-str)))
             sessions)))))
     (t
      (let* ((output (shell-command-to-string "gemini --list-sessions"))
             (lines (seq-filter (lambda (s) (string-match "\\S-" s))
                                (split-string output "\n" t)))
             (data-lines (seq-drop lines 1))
             (candidates nil))
        (dolist (line data-lines)
          (let ((trimmed (string-trim line)))
            (when (string-match
                   (rx (group (one-or-more digit))
                       (one-or-more whitespace)
                       (group (one-or-more nonl)))
                   trimmed)
              (push (cons (match-string 2 trimmed)
                          (match-string 1 trimmed))
                    candidates))))
        (nreverse candidates))))))

(defun fscotto/project-gemini-session ()
  "Choose and resume a Gemini session for the current project."
  (interactive)
  (let* ((project-directory (fscotto/project-root))
         (candidates (fscotto/gemini-session-candidates project-directory)))
    (unless candidates
      (user-error "No Gemini sessions found for %s" project-directory))
    (let* ((selection (completing-read "Gemini session: " candidates nil t))
           (session-idx (cdr (assoc selection candidates))))
      (fscotto/launch-external-terminal
       (list "gemini" "--resume" session-idx)
       project-directory))))

(defun fscotto/project-agent-session ()
  "Choose an agent and resume a saved session for the current project."
  (interactive)
  (let ((agent (completing-read "Agent session: " '("Claude" "Codex" "Gemini" "OpenCode") nil t)))
    (pcase agent
      ("Claude"
       (fscotto/launch-external-terminal '("claude" "--resume")))
      ("OpenCode"
       (fscotto/project-opencode-session))
      ("Codex"
       (fscotto/launch-external-terminal '("codex" "resume")))
      ("Gemini"
       (fscotto/project-gemini-session)))))

(defun fscotto/project-external-terminal ()
  "Open the external terminal in project root."
  (interactive)
  (fscotto/launch-external-terminal))

(defun fscotto/project-opencode ()
  "Open the external terminal in project root and run opencode."
  (interactive)
  (fscotto/launch-external-terminal '("opencode")))

(defun fscotto/project-magit-status ()
  "Open magit-status in project root."
  (interactive)
  (let ((default-directory (fscotto/project-root)))
    (magit-status)))

(defun fscotto/magit-dispatch ()
  "Load Magit if necessary and open magit-dispatch."
  (interactive)
  (require 'magit)
  (call-interactively #'magit-dispatch))

(defun fscotto/disable-c-formatting ()
  (setq-local lsp-enable-on-type-formatting nil))

;; Golang development support functions
(defun fscotto/go-format-on-save ()
  "Format Go buffers on save using gofmt."
  (add-hook 'before-save-hook #'lsp-format-buffer nil t))

(defun fscotto/go-mod-tidy ()
  "Esegue go mod tidy nella root del progetto."
  (interactive)
  (let ((default-directory (project-root (project-current t))))
    (compile "go mod tidy")))

(defun fscotto/go-mod-download ()
  "Scarica i moduli Go."
  (interactive)
  (let ((default-directory (project-root (project-current t))))
    (compile "go mod download")))

(defun fscotto/go-mod-after-save ()
  (when (and (eq major-mode 'go-mod-ts-mode)
             (lsp-workspaces))
    (lsp-restart-workspace)))

(defun fscotto/go-test-package ()
  "Run `go test` in the current package."
  (interactive)
  (let ((default-directory (project-root (project-current t))))
    (compile "go test")))

(defun fscotto/go-test-module ()
  "Run `go test ./...` in the current Go module."
  (interactive)
  (let ((default-directory (project-root (project-current t))))
    (compile "go test ./...")))

(defun fscotto/go-test-current-test ()
  "Run `go test -run` for the test at point."
  (interactive)
  (let* ((test-name (thing-at-point 'symbol t))
         (default-directory (project-root (project-current t))))
    (unless test-name
      (user-error "No test name at point"))
    (compile (format "go test -run '^%s$'" test-name))))

(defun fscotto/go-test-at-point ()
  "Return Go test name at point."
  (let ((sym (thing-at-point 'symbol t)))
    (unless (and sym (string-prefix-p "Test" sym))
      (user-error "No Go test at point"))
    sym))

(defun fscotto/sudo-edit (&optional arg)
  "Reopen current file as root via TRAMP.
If ARG is provided (C-u), prompts for file path."
  (interactive "P")
  (if arg
      (find-file (concat "/sudo:root@localhost:" (read-file-name "File: ")))
    (if buffer-file-name
        (find-alternate-file (concat "/sudo:root@localhost:" buffer-file-name))
      (user-error "This buffer is not associated with a file"))))
