;;; org-social-ui-utils.el --- Utility functions for Org-social UI -*- lexical-binding: t -*- -*- coding: utf-8 -*-

;; SPDX-License-Identifier: GPL-3.0
;; Author: Andros Fenollosa <hi@andros.dev>
;; URL: https://git.andros.dev/org-social/org-social.el

;;; Commentary:
;; Utility functions for formatting, images, and 'org-mode' syntax.

;;; Code:

(require 'org-social-variables)
(require 'org-social-ui-core)
(require 'org-social-file)
(require 'cl-lib)

;; Forward declarations
(declare-function request "request" (url &rest args))
(declare-function org-social-file--new-post "org-social-file" (&optional reply-url reply-id group-context))
(declare-function org-social-file--new-poll "org-social-file" ())
(declare-function org-social-file--new-reaction "org-social-file" (reply-url reply-id emoji))
(declare-function org-social-file--new-boost "org-social-file" (post-url post-id &optional comment))
(declare-function emojify-completing-read "emojify" (&optional prompt))
(declare-function org-social-ui-timeline "org-social-ui-timeline" ())
(declare-function org-social-ui-notifications "org-social-ui-notifications" ())
(declare-function org-social-ui-groups "org-social-ui-groups" ())
(declare-function org-social-ui-search "org-social-ui-search" ())
(declare-function org-social-parser--get-posts-from-feed "org-social-parser" (feed))
(declare-function org-social-parser--get-value "org-social-parser" (feed key))
(declare-function org-social-ui-thread "org-social-ui-thread" (post-url))
(declare-function org-social-ui--post-component "org-social-ui-components" (post &optional full-timeline no-truncate))
(declare-function json-read-from-string "json" (string))
(declare-function org-ctrl-c-ctrl-c "org" (&optional arg))
(declare-function org-table-recalculate "org-table" (&optional all noalign))
(declare-function org-babel-execute-src-block "ob-core" (&optional arg info params executor-type))

;; Thread tracking variables (defined in org-social-ui-thread.el)
(defvar org-social-ui--thread-stack)
(defvar org-social-ui--thread-level)

;; Image Constants
(defconst org-social-ui--regex-image "\\bhttps?:\\/\\/[^][()[:space:]]+\\.\\(?:png\\|jpe?g\\|gif\\)\\b"
  "Regex pattern to match image URLs (PNG, JPG, JPEG, GIF).")

;;; Interactive Org Mode Content

(defvar org-social-ui--org-content-keymap
  (let ((map (make-sparse-keymap)))
    ;; Table commands
    (define-key map (kbd "C-c C-c") 'org-social-ui--org-ctrl-c-ctrl-c)
    (define-key map (kbd "C-c *") 'org-social-ui--org-table-recalculate)
    (define-key map (kbd "TAB") 'org-social-ui--org-cycle)
    (define-key map (kbd "<tab>") 'org-social-ui--org-cycle)
    (define-key map (kbd "S-TAB") 'org-social-ui--org-shifttab)
    (define-key map (kbd "<S-tab>") 'org-social-ui--org-shifttab)
    (define-key map (kbd "<backtab>") 'org-social-ui--org-shifttab)
    ;; Source block commands
    (define-key map (kbd "C-c C-v C-e") 'org-social-ui--org-babel-execute)
    map)
  "Keymap for interactive Org mode content regions in posts.")

(defun org-social-ui--get-org-content-region ()
  "Get the bounds of the Org content region at point.
Returns (START . END) or nil if not in an Org content region."
  (let ((start (point))
        (region-start nil)
        (region-end nil))
    ;; Search backward for region start
    (save-excursion
      (while (and (not region-start) (not (bobp)))
        (if (get-text-property (point) 'org-social-org-content)
            (backward-char)
          (setq region-start (1+ (point)))))
      (when (and (not region-start) (get-text-property (point-min) 'org-social-org-content))
        (setq region-start (point-min))))
    ;; Search forward for region end
    (save-excursion
      (goto-char start)
      (while (and (not region-end) (not (eobp)))
        (if (get-text-property (point) 'org-social-org-content)
            (forward-char)
          (setq region-end (point))))
      (when (and (not region-end) (get-text-property (point-max) 'org-social-org-content))
        (setq region-end (point-max))))
    (when (and region-start region-end)
      (cons region-start region-end))))

(defun org-social-ui--execute-in-org-buffer (content-text callback)
  "Execute CALLBACK in a temporary `org-mode' buffer with CONTENT-TEXT.
CALLBACK is called with no arguments in the `org-mode' buffer.
Returns the buffer content after execution."
  (with-temp-buffer
    (insert content-text)
    (org-mode)
    (goto-char (point-min))
    (funcall callback)
    (buffer-string)))

(defun org-social-ui--refresh-org-content-region (region-start region-end)
  "Refresh the Org content region from REGION-START to REGION-END.
Reapplies overlays and formatting after content changes."
  (let ((inhibit-read-only t))
    ;; Remove old overlays (except keymap overlay)
    (dolist (overlay (overlays-in region-start region-end))
      (when (and (overlay-get overlay 'org-social-overlay)
                 (not (overlay-get overlay 'org-social-keymap-overlay)))
        (delete-overlay overlay)))
    ;; Create keymap overlay if it doesn't exist
    (unless (cl-some (lambda (ov) (overlay-get ov 'org-social-keymap-overlay))
                     (overlays-in region-start region-end))
      (let ((keymap-overlay (make-overlay region-start region-end)))
        (overlay-put keymap-overlay 'keymap org-social-ui--org-content-keymap)
        (overlay-put keymap-overlay 'priority 50)
        (overlay-put keymap-overlay 'org-social-keymap-overlay t)))
    ;; Reapply org-mode styling
    (org-social-ui--apply-org-mode-to-region region-start region-end)))

(defun org-social-ui--org-ctrl-c-ctrl-c ()
  "Execute `org-ctrl-c-ctrl-c' in the Org content region at point."
  (interactive)
  (let ((region (org-social-ui--get-org-content-region)))
    (if region
        (let* ((region-start (car region))
               (region-end (cdr region))
               (content-text (buffer-substring-no-properties region-start region-end))
               (relative-point (- (point) region-start))
               (inhibit-read-only t)
               (new-content (org-social-ui--execute-in-org-buffer
                             content-text
                             (lambda ()
                               (goto-char (+ (point-min) relative-point))
                               (org-ctrl-c-ctrl-c)))))
          ;; Replace content
          (delete-region region-start region-end)
          (goto-char region-start)
          (insert new-content)
          ;; Restore text property
          (put-text-property region-start (point) 'org-social-org-content t)
          ;; Refresh styling (this will restore keymap overlay)
          (org-social-ui--refresh-org-content-region region-start (point))
          (goto-char (+ region-start relative-point))
          (message "Org command executed"))
      (message "Not in an Org content region"))))

(defun org-social-ui--org-table-recalculate ()
  "Recalculate table in the Org content region at point."
  (interactive)
  (let ((region (org-social-ui--get-org-content-region)))
    (if region
        (let* ((region-start (car region))
               (region-end (cdr region))
               (content-text (buffer-substring-no-properties region-start region-end))
               (relative-point (- (point) region-start))
               (inhibit-read-only t)
               (new-content (org-social-ui--execute-in-org-buffer
                             content-text
                             (lambda ()
                               (goto-char (+ (point-min) relative-point))
                               (org-table-recalculate 'all)))))
          ;; Replace content
          (delete-region region-start region-end)
          (goto-char region-start)
          (insert new-content)
          ;; Restore text property
          (put-text-property region-start (point) 'org-social-org-content t)
          ;; Refresh styling (this will restore keymap overlay)
          (org-social-ui--refresh-org-content-region region-start (point))
          (goto-char (+ region-start relative-point))
          (message "Table recalculated"))
      (message "Not in an Org content region"))))

(defun org-social-ui--org-cycle ()
  "Cycle visibility in the Org content region at point."
  (interactive)
  (let ((region (org-social-ui--get-org-content-region)))
    (if region
        (progn
          ;; For now, just show a message
          ;; Full cycling would require tracking fold state
          (message "Org cycling (folding) in posts not yet fully implemented"))
      (message "Not in an Org content region"))))

(defun org-social-ui--org-shifttab ()
  "Global cycle visibility in the Org content region."
  (interactive)
  (message "Global Org cycling in posts not yet implemented"))

(defun org-social-ui--org-babel-execute ()
  "Execute source block in the Org content region at point."
  (interactive)
  (let ((region (org-social-ui--get-org-content-region)))
    (if region
        (let* ((region-start (car region))
               (region-end (cdr region))
               (content-text (buffer-substring-no-properties region-start region-end))
               (relative-point (- (point) region-start))
               (inhibit-read-only t)
               (new-content (org-social-ui--execute-in-org-buffer
                             content-text
                             (lambda ()
                               (goto-char (+ (point-min) relative-point))
                               (org-babel-execute-src-block)))))
          ;; Replace content
          (delete-region region-start region-end)
          (goto-char region-start)
          (insert new-content)
          ;; Restore text property
          (put-text-property region-start (point) 'org-social-org-content t)
          ;; Refresh styling (this will restore keymap overlay)
          (org-social-ui--refresh-org-content-region region-start (point))
          (goto-char (+ region-start relative-point))
          (message "Source block executed"))
      (message "Not in an Org content region"))))

(defun org-social-ui--format-org-headings (text)
  "Format `org-mode' headings in TEXT to be more visually appealing.
Replaces *** and deeper headings with visual markers, promoting them
to account for the structure of the social.org file."
  (let ((lines (split-string text "\n")))
    (mapconcat
     (lambda (line)
       (cond
        ;; Level 6 heading: ****** → ▸▸▸▸
        ((string-match "^\\(\\*\\{6,\\}\\) \\(.+\\)$" line)
         (concat "▸▸▸▸ " (match-string 2 line)))
        ;; Level 5 heading: ***** → ▸▸▸
        ((string-match "^\\(\\*\\{5\\}\\) \\(.+\\)$" line)
         (concat "▸▸▸ " (match-string 2 line)))
        ;; Level 4 heading: **** → ▸▸
        ((string-match "^\\(\\*\\{4\\}\\) \\(.+\\)$" line)
         (concat "▸▸ " (match-string 2 line)))
        ;; Level 3 heading: *** → ▸
        ((string-match "^\\(\\*\\{3\\}\\) \\(.+\\)$" line)
         (concat "▸ " (match-string 2 line)))
        ;; Default: return line as is
        (t line)))
     lines
     "\n")))

(defun org-social-ui--insert-formatted-text (text &optional size font-color background-color)
  "Insert TEXT with optional formatting SIZE, FONT-COLOR, and BACKGROUND-COLOR."
  (let ((start (point)))
    (let ((inhibit-read-only t))
      (insert text))
    (let ((end (point))
          (props (list)))
      (when size
        (push `(:height ,size) props))
      (when font-color
        (push `(:foreground ,font-color) props))
      (when background-color
        (push `(:background ,background-color) props))
      (when props
        (put-text-property start end 'face (apply #'append props))))))

(defun org-social-ui--insert-logo ()
  "Insert the Org Social logo/header."
  (let* ((base-dir (cond
                    (load-file-name (file-name-directory load-file-name))
                    (buffer-file-name (file-name-directory buffer-file-name))
                    ((boundp 'org-social--root-dir) org-social--root-dir)
                    (t default-directory)))
         (logo-path (expand-file-name "org-social-logo.png" base-dir)))
    ;; Try to insert image if available, otherwise fallback to text
    (condition-case nil
        (if (and (display-graphic-p)
                 (file-exists-p logo-path))
            (progn
              (org-social-ui--insert-formatted-text "\n")
              (insert-image (create-image logo-path nil nil :height 60))
              (org-social-ui--insert-formatted-text " ")
              (org-social-ui--insert-formatted-text "Org Social" 1.3 "#4a90e2")
              (org-social-ui--insert-formatted-text "\n\n"))
          ;; Fallback to text logo
          (progn
            (org-social-ui--insert-formatted-text "\n🐉 " 1.5 "#4a90e2")
            (org-social-ui--insert-formatted-text "Org Social" 1.3 "#4a90e2")
            (org-social-ui--insert-formatted-text "\n\n")))
      (error
       ;; If anything fails, use simple text fallback
       (org-social-ui--insert-formatted-text "\n🐉 " 1.5 "#4a90e2")
       (org-social-ui--insert-formatted-text "Org Social" 1.3 "#4a90e2")
       (org-social-ui--insert-formatted-text "\n\n")))))

(defun org-social-ui--string-separator ()
  "Return a string with the separator character."
  (make-string 75 org-social-ui--char-separator))

(defun org-social-ui--insert-separator ()
  "Insert a horizontal separator line."
  (org-social-ui--insert-formatted-text "\n")
  (org-social-ui--insert-formatted-text (org-social-ui--string-separator) nil "#666666")
  (org-social-ui--insert-formatted-text "\n"))

;;; Image Functions

(defun org-social-ui--open-image-in-buffer (url)
  "Open image from URL in a new buffer at full size."
  (interactive)
  (when (and url (stringp url))
    ;; Ensure image is cached
    (unless (org-social-ui--cache-image-p url)
      (org-social-ui--cache-image url))
    ;; Get image file path
    (let ((image-file (expand-file-name
                       (base64-encode-string url :no-line-break)
                       org-social-image-cache-directory)))
      (when (file-exists-p image-file)
        ;; Create new buffer for image
        (let* ((buffer-name (format "*Image: %s*" (file-name-nondirectory url)))
               (buffer (get-buffer-create buffer-name)))
          (with-current-buffer buffer
            (let ((inhibit-read-only t))
              (erase-buffer)
              ;; Insert image at full size
              (condition-case err-msg
                  (progn
                    (insert-image (create-image image-file nil nil :max-width (window-pixel-width) :max-height (window-pixel-height)))
                    (insert "\n\n")
                    (insert (propertize (format "URL: %s\n" url) 'face '(:foreground "#666666")))
                    (insert (propertize "Press 'q' to close this buffer" 'face '(:foreground "#888888"))))
                (error
                 (insert (format "Error displaying image: %s\n" (error-message-string err-msg)))
                 (insert (format "URL: %s\n" url))))
              ;; Setup buffer
              (setq buffer-read-only t)
              (local-set-key (kbd "q") 'kill-current-buffer)
              (local-set-key (kbd "Q") 'kill-current-buffer)
              (goto-char (point-min))))
          ;; Switch to the image buffer
          (switch-to-buffer buffer))))))

(defun org-social-ui--image-p (text)
  "Check if TEXT contain an image URL."
  (and text (stringp text) (string-match-p org-social-ui--regex-image text)))

(defun org-social-ui--cache-image-p (url)
  "Check if an image from URL is already cached."
  (when (and url (stringp url))
    (file-exists-p (expand-file-name
                    (base64-encode-string url :no-line-break)
                    org-social-image-cache-directory))))

(defun org-social-ui--cache-image (url &optional callback)
  "Download an image from URL to cache.
Optional CALLBACK is called with success status when download completes."
  (when (and url (stringp url))
    (unless (file-exists-p org-social-image-cache-directory)
      (make-directory org-social-image-cache-directory t))
    (require 'request nil t)
    (if (featurep 'request)
        (request url
                 :type "GET"
                 :sync t
                 :parser 'buffer-string
                 :success (cl-function
                           (lambda (&key data &allow-other-keys)
                             (let ((filename-image (base64-encode-string url :no-line-break)))
                               (with-temp-file (expand-file-name filename-image org-social-image-cache-directory)
                                 (set-buffer-file-coding-system 'binary)
                                 (insert data))
                               (when callback (funcall callback t)))))
                 :error (cl-function
                         (lambda (&key error-thrown &allow-other-keys)
                           (message "Error downloading image: %S" error-thrown)
                           (when callback (funcall callback nil)))))
      (progn
        (message "Image caching requires the 'request' package")
        (when callback (funcall callback nil))))))

(defun org-social-ui--put-image-from-cache (url _pos &optional width)
  "Put an image from cache at URL at position _POS with optional WIDTH."
  (when (and url (stringp url) (display-graphic-p))
    (unless (org-social-ui--cache-image-p url)
      (org-social-ui--cache-image url))
    (when (org-social-ui--cache-image-p url)
      (let ((image-file (expand-file-name
                         (base64-encode-string url :no-line-break)
                         org-social-image-cache-directory)))
        (condition-case err
            (let ((image-props (append (when width (list :width width))
                                       (list :ascent 'center))))
              (insert-image (apply #'create-image image-file nil nil image-props) " "))
          (error
           (message "Error displaying image: %S" err)
           (org-social-ui--insert-formatted-text "🖼️ [Image]" nil "#666666")))))))

(defun org-social-ui--apply-org-mode-to-region (start end)
  "Apply `org-mode' syntax highlighting from START to END using overlays."
  (save-excursion
    (save-restriction
      (narrow-to-region start end)
      (goto-char start)

      ;; Create overlays with higher priority than widgets for 'org-mode' syntax
      ;; Bold text: **text**
      (goto-char start)
      (while (re-search-forward "\\*\\*\\([^*\n]+\\)\\*\\*" end t)
        (let ((overlay (make-overlay (match-beginning 1) (match-end 1))))
          (overlay-put overlay 'face 'bold)
          (overlay-put overlay 'priority 100)
          (overlay-put overlay 'org-social-overlay t))) ; Mark as org-social overlay

      ;; Italic text: /text/
      (goto-char start)
      (while (re-search-forward "/\\([^/\n]+\\)/" end t)
        (let ((overlay (make-overlay (match-beginning 1) (match-end 1))))
          (overlay-put overlay 'face 'italic)
          (overlay-put overlay 'priority 100)
          (overlay-put overlay 'org-social-overlay t)))

      ;; Code text: =text=
      (goto-char start)
      (while (re-search-forward "=\\([^=\n]+\\)=" end t)
        (let ((overlay (make-overlay (match-beginning 1) (match-end 1))))
          (overlay-put overlay 'face 'org-code)
          (overlay-put overlay 'priority 100)
          (overlay-put overlay 'org-social-overlay t)))

      ;; Verbatim text: ~text~
      (goto-char start)
      (while (re-search-forward "~\\([^~\n]+\\)~" end t)
        (let ((overlay (make-overlay (match-beginning 1) (match-end 1))))
          (overlay-put overlay 'face 'org-verbatim)
          (overlay-put overlay 'priority 100)
          (overlay-put overlay 'org-social-overlay t)))

      ;; Strike-through: +text+
      (goto-char start)
      (while (re-search-forward "\\+\\([^+\n]+\\)\\+" end t)
        (let ((overlay (make-overlay (match-beginning 1) (match-end 1))))
          (overlay-put overlay 'face '(:strike-through t))
          (overlay-put overlay 'priority 100)
          (overlay-put overlay 'org-social-overlay t)))

      ;; Underline: _text_
      (goto-char start)
      (while (re-search-forward "_\\([^_\n]+\\)_" end t)
        (let ((overlay (make-overlay (match-beginning 1) (match-end 1))))
          (overlay-put overlay 'face 'underline)
          (overlay-put overlay 'priority 100)
          (overlay-put overlay 'org-social-overlay t)))

      ;; Links: [[url][description]] or [[url]]
      (goto-char start)
      (while (re-search-forward "\\[\\[\\([^]]+\\)\\]\\(?:\\[\\([^]]+\\)\\]\\)?\\]" end t)
        (let* ((url (match-string 1))
               (desc (match-string 2))
               (display-text (or desc url))
               (link-start (match-beginning 0))
               (link-end (match-end 0))
               (is-image (string-match-p org-social-ui--regex-image url)))
          ;; Check if URL is an image
          (if is-image
              ;; Handle image link
              (progn
                ;; Delete the link syntax
                (delete-region link-start link-end)
                (goto-char link-start)
                ;; Insert newline before image for better spacing
                (insert "\n")
                ;; Try to display the image inline
                (let ((image-start (point)))
                  (condition-case nil
                      (progn
                        ;; Use existing cache function to download and display image
                        (org-social-ui--put-image-from-cache url nil 400)
                        ;; Add newline after image
                        (insert "\n")
                        ;; Create overlay on the image for click functionality
                        (let ((image-overlay (make-overlay image-start (point)))
                              (keymap (make-sparse-keymap)))
                          ;; Setup keymap for clicking on image
                          (define-key keymap (kbd "RET") `(lambda () (interactive) (org-social-ui--open-image-in-buffer ,url)))
                          (define-key keymap (kbd "<mouse-1>") `(lambda () (interactive) (org-social-ui--open-image-in-buffer ,url)))
                          (define-key keymap (kbd "<mouse-2>") `(lambda () (interactive) (org-social-ui--open-image-in-buffer ,url)))
                          (overlay-put image-overlay 'keymap keymap)
                          (overlay-put image-overlay 'mouse-face 'highlight)
                          (overlay-put image-overlay 'priority 100)
                          (overlay-put image-overlay 'org-social-overlay t)
                          (overlay-put image-overlay 'help-echo "Click to open image in full size")))
                    (error
                     ;; If image fails to load, show fallback text
                     (goto-char image-start)
                     (insert (format "🖼️ [Image: %s]\n" (or desc url)))
                     (let ((fallback-overlay (make-overlay image-start (point)))
                           (keymap (make-sparse-keymap)))
                       (define-key keymap (kbd "RET") `(lambda () (interactive) (eww ,url)))
                       (define-key keymap (kbd "<mouse-1>") `(lambda () (interactive) (eww ,url)))
                       (overlay-put fallback-overlay 'face 'org-link)
                       (overlay-put fallback-overlay 'mouse-face 'highlight)
                       (overlay-put fallback-overlay 'priority 100)
                       (overlay-put fallback-overlay 'org-social-overlay t)
                       (overlay-put fallback-overlay 'keymap keymap)
                       (overlay-put fallback-overlay 'help-echo (format "Visit: %s" url))
                       (overlay-put fallback-overlay 'org-social-url url))))))
            ;; Handle regular link (not an image)
            (progn
              ;; Replace the entire link syntax with just the display text
              (delete-region link-start link-end)
              (goto-char link-start)
              (insert display-text)
              ;; Create overlay for the display text with click functionality
              (let ((overlay (make-overlay link-start (+ link-start (length display-text))))
                    (keymap (make-sparse-keymap)))
                ;; Setup keymap for clicking
                (define-key keymap (kbd "RET") `(lambda () (interactive) (eww ,url)))
                (define-key keymap (kbd "<mouse-1>") `(lambda () (interactive) (eww ,url)))
                (define-key keymap (kbd "<mouse-2>") `(lambda () (interactive) (eww ,url)))
                ;; Apply properties to overlay
                (overlay-put overlay 'face 'org-link)
                (overlay-put overlay 'mouse-face 'highlight)
                (overlay-put overlay 'priority 100)
                (overlay-put overlay 'org-social-overlay t)
                (overlay-put overlay 'keymap keymap)
                (overlay-put overlay 'help-echo (format "Visit: %s" url))
                ;; Store URL for reference
                (overlay-put overlay 'org-social-url url))))))

      ;; Hashtags: No longer needed - now handled by org-social-ui--insert-formatted-text
      ;; (Hashtag coloring moved to use same technique as name/date/client)

      ;; Formatted headings: ▸▸▸ Title (from 'org-mode' headings)
      (goto-char start)
      (while (re-search-forward "^\\(▸+\\) \\(.+\\)$" end t)
        (let ((marker-overlay (make-overlay (match-beginning 1) (match-end 1)))
              (title-overlay (make-overlay (match-beginning 2) (match-end 2))))
          ;; Style the marker (▸▸▸)
          (overlay-put marker-overlay 'face '(:foreground "#4a90e2" :weight bold))
          (overlay-put marker-overlay 'priority 100)
          (overlay-put marker-overlay 'org-social-overlay t)
          ;; Style the title text
          (overlay-put title-overlay 'face '(:foreground "#4a90e2" :weight bold :height 1.1))
          (overlay-put title-overlay 'priority 100)
          (overlay-put title-overlay 'org-social-overlay t)))

      ;; Plain URLs: https://... or http://...
      ;; Process from end to start to avoid issues with changing positions
      (goto-char start)
      (let ((url-positions '()))
        ;; First, collect all URL positions
        (while (re-search-forward "\\(https?://[^ \t\n<>\"]+\\)" end t)
          (push (cons (match-beginning 1) (cons (match-end 1) (match-string 1))) url-positions))
        ;; Then create overlays (avoiding duplicates)
        (dolist (url-info url-positions)
          (let* ((url-start (car url-info))
                 (url-end (cadr url-info))
                 (url (cddr url-info))
                 ;; Check if there's already an overlay here
                 (existing-overlays (overlays-at url-start))
                 (has-link-overlay (cl-some (lambda (ov) (overlay-get ov 'org-social-url)) existing-overlays)))
            ;; Only create overlay if there isn't one already
            (unless has-link-overlay
              (let ((overlay (make-overlay url-start url-end))
                    (keymap (make-sparse-keymap)))
                ;; Setup keymap for clicking
                (define-key keymap (kbd "RET") `(lambda () (interactive) (eww ,url)))
                (define-key keymap (kbd "<mouse-1>") `(lambda () (interactive) (eww ,url)))
                (define-key keymap (kbd "<mouse-2>") `(lambda () (interactive) (eww ,url)))
                ;; Apply properties to overlay
                (overlay-put overlay 'face 'org-link)
                (overlay-put overlay 'mouse-face 'highlight)
                (overlay-put overlay 'priority 100)
                (overlay-put overlay 'org-social-overlay t)
                (overlay-put overlay 'keymap keymap)
                (overlay-put overlay 'help-echo (format "Visit: %s" url))
                (overlay-put overlay 'org-social-url url))))))

      ;; List items: - item or + item or * item
      (goto-char start)
      (while (re-search-forward "^\\s-*\\([-+*]\\)\\s-+" end t)
        (let ((overlay (make-overlay (match-beginning 1) (match-end 1))))
          (overlay-put overlay 'face 'org-list-dt)
          (overlay-put overlay 'priority 100)
          (overlay-put overlay 'org-social-overlay t)))

      ;; Tables: | cell | cell | (highlight table delimiters)
      (goto-char start)
      (while (re-search-forward "^\\s-*\\(|.*|\\)\\s-*$" end t)
        (let ((line-start (match-beginning 1))
              (line-end (match-end 1)))
          ;; Highlight the entire table row
          (let ((overlay (make-overlay line-start line-end)))
            (overlay-put overlay 'face 'org-table)
            (overlay-put overlay 'priority 95)
            (overlay-put overlay 'org-social-overlay t))
          ;; Highlight individual separators
          (save-excursion
            (goto-char line-start)
            (while (re-search-forward "|" line-end t)
              (let ((overlay (make-overlay (match-beginning 0) (match-end 0))))
                (overlay-put overlay 'face '(:foreground "#888888" :weight bold))
                (overlay-put overlay 'priority 100)
                (overlay-put overlay 'org-social-overlay t)))))))))

;; (defun org-social-ui--refresh-hashtag-colors ()
;;   "Force refresh of hashtag colors in current buffer."
;;   ;; No longer needed - hashtags now use org-social-ui--insert-formatted-text
;;   ;; like name/date/client which work perfectly
;;   (interactive)
;;   (message "Hashtag colors now use org-social-ui--insert-formatted-text - no refresh needed"))

(defun org-social-ui--format-relative-time (timestamp)
  "Format TIMESTAMP as relative time."
  (condition-case nil
      (let* ((time (if (stringp timestamp)
                       (date-to-time timestamp)
                     timestamp))
             (diff (float-time (time-subtract (current-time) time)))
             (days (floor (/ diff 86400)))
             (hours (floor (/ (mod diff 86400) 3600)))
             (minutes (floor (/ (mod diff 3600) 60))))
        (cond
         ((> days 0) (format "%d day%s ago" days (if (= days 1) "" "s")))
         ((> hours 0) (format "%d hour%s ago" hours (if (= hours 1) "" "s")))
         ((> minutes 0) (format "%d minute%s ago" minutes (if (= minutes 1) "" "s")))
         (t "Just now")))
    (error "Unknown time")))

;;; Navigation Functions

(defun org-social-ui--goto-next-post ()
  "Go to the next post or group button."
  (interactive)
  ;; Check if we're in groups buffer
  (if (eq org-social-ui--current-screen 'groups)
      (org-social-ui--goto-next-group-button)
    ;; Normal post navigation
    (let ((separator-regex (concat "^" (regexp-quote (org-social-ui--string-separator)) "$"))
          (was-at-last nil))
      ;; Try to find next separator
      (if (search-forward-regexp separator-regex nil t)
          (progn
            (forward-line 1)
            ;; Check if we've reached the last post
            (when (org-social-ui--last-separator-p)
              (setq was-at-last t)
              (run-hooks 'org-social-ui--last-post-hook)))
        ;; No separator found, we're past all posts
        (setq was-at-last t))

      ;; If we're at the last post, try to load more
      (when was-at-last
        ;; Search for "Show more" button from the end of buffer (where it always is)
        (unless (save-excursion
                  (goto-char (point-max))
                  (when (search-backward "Show more" nil t)
                    (let ((widget (widget-at (point))))
                      (when (and widget (eq (widget-type widget) 'push-button))
                        (widget-button-press (point))
                        t))))
          (message "No more posts to load")))

      ;; Center the screen on cursor position
      (recenter))))

(defun org-social-ui--goto-previous-post ()
  "Go to the previous post or group button."
  (interactive)
  ;; Check if we're in groups buffer
  (if (eq org-social-ui--current-screen 'groups)
      (org-social-ui--goto-previous-group-button)
    ;; Normal post navigation
    (let ((separator-regex (concat "^" (regexp-quote (org-social-ui--string-separator)) "$")))
      (if (search-backward-regexp separator-regex nil t)
          (progn
            (if (search-backward-regexp separator-regex nil t)
                (forward-line 1)
              (progn
                (goto-char (point-min))
                ;; Only move forward if there's content to move to
                (when (> (point-max) (point-min))
                  (forward-line 1)))))
        (progn
          (goto-char (point-min))
          (message "Already at first post")))
      ;; Center the screen on cursor position
      (recenter))))

(defun org-social-ui--goto-next-group-button ()
  "Go to the next View Posts button in groups buffer."
  (let ((found nil)
        (start-point (point))
        (current-widget (widget-at (point))))
    ;; If we're on a "View Posts" button, skip past it first
    (when (and current-widget (eq (widget-type current-widget) 'push-button))
      (let* ((widget-start (widget-get current-widget :from))
             (widget-end (widget-get current-widget :to))
             (widget-text (when (and widget-start widget-end)
                            (buffer-substring-no-properties widget-start widget-end))))
        (when (and widget-text (string-match-p "View Posts" widget-text))
          ;; Move past this widget
          (goto-char widget-end))))

    ;; Search forward for next "View Posts" button
    (while (and (not found) (not (eobp)))
      (forward-char 1)
      (let ((widget (widget-at (point))))
        (when (and widget (eq (widget-type widget) 'push-button))
          (let* ((widget-start (widget-get widget :from))
                 (widget-end (widget-get widget :to))
                 (widget-text (when (and widget-start widget-end)
                                (buffer-substring-no-properties widget-start widget-end))))
            (when (and widget-text (string-match-p "View Posts" widget-text))
              ;; Move to the start of the widget for consistency
              (goto-char widget-start)
              (setq found t))))))
    (if found
        (recenter)
      (goto-char start-point)
      (message "No more groups"))))

(defun org-social-ui--goto-previous-group-button ()
  "Go to the previous View Posts button in groups buffer."
  (let ((found nil)
        (start-point (point))
        (current-widget (widget-at (point))))
    ;; If we're on a "View Posts" button, skip before it first
    (when (and current-widget (eq (widget-type current-widget) 'push-button))
      (let* ((widget-start (widget-get current-widget :from))
             (widget-end (widget-get current-widget :to))
             (widget-text (when (and widget-start widget-end)
                            (buffer-substring-no-properties widget-start widget-end))))
        (when (and widget-text (string-match-p "View Posts" widget-text))
          ;; Move before this widget
          (goto-char widget-start))))

    ;; Search backward for previous "View Posts" button
    (while (and (not found) (not (bobp)))
      (backward-char 1)
      (let ((widget (widget-at (point))))
        (when (and widget (eq (widget-type widget) 'push-button))
          (let* ((widget-start (widget-get widget :from))
                 (widget-end (widget-get widget :to))
                 (widget-text (when (and widget-start widget-end)
                                (buffer-substring-no-properties widget-start widget-end))))
            (when (and widget-text (string-match-p "View Posts" widget-text))
              ;; Move to the start of the widget for consistency
              (goto-char widget-start)
              (setq found t))))))
    (if found
        (recenter)
      (goto-char start-point)
      (message "No previous groups"))))

(defun org-social-ui--last-separator-p ()
  "Check if we're at the last separator (near bottom of posts)."
  (save-excursion
    (let ((separator-regex (concat "^" (regexp-quote (org-social-ui--string-separator)) "$")))
      (not (search-forward-regexp separator-regex nil t)))))


;;; Action Functions

(defun org-social-ui--new-post ()
  "Create a new post.
If called from a group buffer, automatically adds GROUP property."
  (interactive)
  ;; Check if we're in a group buffer by buffer name
  (let ((group-ctx nil)
        (buffer-name (buffer-name)))
    (when (string-match "^\\*Org Social Group: \\(.+\\)\\*$" buffer-name)
      (let ((group-name (match-string 1 buffer-name))
            (relay-url nil))
        ;; Extract relay URL from buffer content (it's in the header)
        (save-excursion
          (goto-char (point-min))
          (when (re-search-forward (concat "👥 " (regexp-quote group-name) " (\\(.+?\\))") nil t)
            (setq relay-url (match-string 1))))
        (when relay-url
          (setq group-ctx `((name . ,group-name)
                            (relay-url . ,relay-url))))))
    (org-social-file--new-post nil nil group-ctx)))

(defun org-social-ui--new-poll ()
  "Create a new poll."
  (interactive)
  (org-social-file--new-poll))

(defun org-social-ui--reply-to-post ()
  "Reply to the post by pressing the Reply button."
  (interactive)
  (unless (org-social-ui--find-and-press-button "↳")
    (message "No reply button found near point")))

(defun org-social-ui--add-reaction-at-point ()
  "Add a reaction to the post by pressing the React button."
  (interactive)
  (unless (org-social-ui--find-and-press-button "😊")
    (message "No react button found near point")))

(defun org-social-ui--boost-post-at-point ()
  "Boost the post at point by pressing the Boost button."
  (interactive)
  (unless (org-social-ui--find-and-press-button "🔄")
    (message "No boost button found near point")))

(defun org-social-ui--add-reaction (author-url timestamp)
  "Add a reaction to a post using emojify selector.
AUTHOR-URL is the URL of the post author.
TIMESTAMP is the timestamp of the post being reacted to."
  (interactive)
  (require 'emojify nil t)
  (if (fboundp 'emojify-completing-read)
      (let ((selected-emoji (emojify-completing-read "Select reaction: ")))
        (when selected-emoji
          (org-social-file--new-reaction author-url timestamp selected-emoji)))
    (message "Emojify not available. Please install the emojify package.")))

(defun org-social-ui--boost-post (author-url timestamp)
  "Boost (share) a post by creating a new post with INCLUDE property.
AUTHOR-URL is the URL of the post author.
TIMESTAMP is the timestamp of the post being boosted."
  (interactive)
  (org-social-file--new-boost author-url timestamp nil))

(defun org-social-ui--get-post-at-point ()
  "Get post data at current point.
Returns the post data alist stored in the widget, or nil if not found."
  (save-excursion
    ;; Search backward from current position to find the nearest item widget
    (let ((found-widget nil)
          (search-limit (max (point-min) (- (point) 5000)))) ; Limit search to 5000 chars back
      (while (and (not found-widget) (> (point) search-limit))
        (let ((widget (widget-at (point))))
          (if (and widget (eq (widget-type widget) 'item))
              (setq found-widget widget)
            (backward-char 1))))
      (when found-widget
        (widget-value found-widget)))))

;;; Screen Navigation Functions

(defun org-social-ui--go-back ()
  "Go back to previous buffer."
  (interactive)
  (let ((previous-buffer (other-buffer (current-buffer) 1)))
    (if previous-buffer
        (switch-to-buffer previous-buffer)
      (org-social-ui-timeline))))

(defun org-social-ui--view-timeline ()
  "Switch to timeline view."
  (interactive)
  (org-social-ui-timeline))

(defun org-social-ui--find-and-press-button (button-text)
  "Search forward for a widget button containing BUTTON-TEXT.
Returns t if button was found and pressed, nil otherwise."
  (save-excursion
    (let ((found nil)
          (search-limit (min (point-max) (+ (point) 10000))))
      ;; Search forward character by character looking for widgets
      (while (and (not found) (< (point) search-limit))
        (forward-char 1)
        (let ((widget (widget-at (point))))
          (when (and widget
                     (eq (widget-type widget) 'push-button))
            ;; Check if the button text contains our search text
            (let* ((widget-start (widget-get widget :from))
                   (widget-end (widget-get widget :to))
                   (widget-text (when (and widget-start widget-end)
                                  (buffer-substring-no-properties widget-start widget-end))))
              (when (and widget-text (string-match-p (regexp-quote button-text) widget-text))
                (widget-button-press (point))
                (setq found t))))))
      found)))

(defun org-social-ui--view-thread ()
  "View thread for current post by pressing the Thread button."
  (interactive)
  (unless (org-social-ui--find-and-press-button "🧵")
    (message "No thread button found near point")))

(defun org-social-ui--view-notifications ()
  "Switch to notifications view."
  (interactive)
  (org-social-ui-notifications))

(defun org-social-ui--view-profile ()
  "View profile for current post by pressing the Profile button."
  (interactive)
  (unless (org-social-ui--find-and-press-button "👤")
    (message "No profile button found near point")))

(defun org-social-ui--view-groups ()
  "Switch to groups view."
  (interactive)
  (org-social-ui-groups))

(defun org-social-ui--view-search ()
  "Switch to search view."
  (interactive)
  (require 'org-social-ui-search)
  (org-social-ui-search))

(defun org-social-ui--refresh ()
  "Refresh current screen."
  (interactive)
  ;; Clear cache to force fresh download
  (setq org-social-variables--feeds nil)
  (setq org-social-variables--queue nil)
  ;; Reset pagination state
  (setq org-social-ui--current-page 1
        org-social-ui--timeline-current-list nil)
  (when org-social-ui--timeline-widget-loading-more
    (setq org-social-ui--timeline-widget-loading-more nil))
  (message "Cache cleared, refreshing...")
  (cond
   ((eq org-social-ui--current-screen 'timeline)
    (org-social-ui-timeline))
   ((eq org-social-ui--current-screen 'notifications)
    (org-social-ui-notifications))
   ((eq org-social-ui--current-screen 'groups)
    (org-social-ui-groups))
   (t (message "Nothing to refresh"))))

(defun org-social-ui--quit ()
  "Quit Org Social UI."
  (interactive)
  (dolist (buffer-name (list org-social-ui--timeline-buffer-name
                             org-social-ui--thread-buffer-name
                             org-social-ui--notifications-buffer-name
                             org-social-ui--profile-buffer-name
                             org-social-ui--groups-buffer-name))
    (when (get-buffer buffer-name)
      (kill-buffer buffer-name)))

  ;; Clear all thread level buffers
  (dolist (buffer (buffer-list))
    (when (string-match-p "\\*Org Social Thread Level [0-9]+\\*" (buffer-name buffer))
      (kill-buffer buffer)))

  ;; Reset thread tracking variables
  (setq org-social-ui--thread-stack nil)
  (setq org-social-ui--thread-level 0))

;;; Thread Helper Functions

(defun org-social-ui--fetch-post-sync (post-url)
  "Fetch post data for POST-URL synchronously.
Returns post data alist or nil if failed."
  (require 'org-social-feed)
  (when (and post-url (stringp post-url))
    (if (string-match "\\(.*\\)#\\(.+\\)$" post-url)
        (let* ((feed-url (match-string 1 post-url))
               (post-id (match-string 2 post-url))
               (buffer (condition-case nil
                           (url-retrieve-synchronously feed-url t nil 10)
                         (error nil))))
          (when buffer
            (with-current-buffer buffer
              (set-buffer-multibyte t)
              (goto-char (point-min))
              (when (re-search-forward "\n\n" nil t)
                (let* ((feed-data (decode-coding-string
                                   (buffer-substring-no-properties (point) (point-max))
                                   'utf-8))
                       (posts (org-social-parser--get-posts-from-feed feed-data))
                       (target-post (cl-find-if
                                     (lambda (post)
                                       (let ((timestamp (or (alist-get 'timestamp post)
                                                            (alist-get 'id post))))
                                         (and timestamp (string= timestamp post-id))))
                                     posts)))
                  (kill-buffer buffer)
                  (when target-post
                    (append target-post
                            `((author-url . ,feed-url)
                              (author-nick . ,(or (org-social-parser--get-value feed-data "NICK") "Unknown"))
                              (feed-avatar . ,(org-social-parser--get-value feed-data "AVATAR"))))))))))
      nil)))

(defun org-social-ui--fetch-replies-sync (post-url)
  "Fetch replies for POST-URL from relay synchronously.
Returns list of reply structures from relay data, or nil if failed."
  (require 'org-social-relay)
  (require 'json)
  (when (and org-social-relay
             (not (string-empty-p org-social-relay)))
    (let* ((relay-url (string-trim-right org-social-relay "/"))
           (encoded-url (url-hexify-string post-url))
           (url (format "%s/replies/?post=%s" relay-url encoded-url))
           (buffer (condition-case nil
                       (url-retrieve-synchronously url t nil 10)
                     (error nil))))
      (when buffer
        (with-current-buffer buffer
          (set-buffer-multibyte t)
          (goto-char (point-min))
          (when (re-search-forward "\n\n" nil t)
            (let* ((json-data (decode-coding-string
                               (buffer-substring-no-properties (point) (point-max))
                               'utf-8))
                   (response (condition-case nil
                                 (json-read-from-string json-data)
                               (error nil)))
                   (response-type (when response (cdr (assoc 'type response))))
                   (replies-data (when response (cdr (assoc 'data response)))))
              (kill-buffer buffer)
              (when (and response-type (string= response-type "Success") replies-data)
                (if (vectorp replies-data)
                    (append replies-data nil)
                  replies-data)))))))))

(defun org-social-ui--fetch-post-reactions-sync (post-url post-data)
  "Fetch interactions (reactions and boosts) for POST-URL from Relay.
Returns POST-DATA with reactions and boosts added.
Returns POST-DATA unchanged if failed."
  (require 'org-social-relay)
  (require 'json)
  (if (and org-social-relay
           (not (string-empty-p org-social-relay)))
      (let* ((relay-url (string-trim-right org-social-relay "/"))
             (encoded-url (url-hexify-string post-url))
             (url (format "%s/interactions/?post=%s" relay-url encoded-url))
             (buffer (condition-case nil
                         (url-retrieve-synchronously url t nil 10)
                       (error nil))))
        (if buffer
            (with-current-buffer buffer
              (set-buffer-multibyte t)
              (goto-char (point-min))
              (if (re-search-forward "\n\n" nil t)
                  (let* ((json-data (decode-coding-string
                                     (buffer-substring-no-properties (point) (point-max))
                                     'utf-8))
                         (response (condition-case nil
                                       (json-read-from-string json-data)
                                     (error nil)))
                         (response-type (when response (cdr (assoc 'type response))))
                         (interactions-data (when response (cdr (assoc 'data response)))))
                    (kill-buffer buffer)
                    (if (and response-type (string= response-type "Success") interactions-data)
                        ;; Extract reactions and boosts from interactions
                        (let* ((reactions-data (cdr (assoc 'reactions interactions-data)))
                               (boosts-data (cdr (assoc 'boosts interactions-data)))
                               (reactions-list (when reactions-data
                                                 (if (vectorp reactions-data)
                                                     (append reactions-data nil)
                                                   reactions-data)))
                               (boosts-list (when boosts-data
                                              (if (vectorp boosts-data)
                                                  (append boosts-data nil)
                                                boosts-data)))
                               ;; Group reactions by emoji
                               (moods-by-emoji (make-hash-table :test 'equal)))
                          ;; Process reactions
                          (when reactions-list
                            (dolist (reaction reactions-list)
                              (let ((emoji (cdr (assoc 'emoji reaction)))
                                    (reaction-post (cdr (assoc 'post reaction))))
                                (when (and emoji reaction-post)
                                  (let ((existing (gethash emoji moods-by-emoji)))
                                    (puthash emoji
                                             (vconcat (vector reaction-post)
                                                      (if (vectorp existing) existing (vector)))
                                             moods-by-emoji))))))
                          ;; Convert hash table to alist format expected by component
                          (let ((moods-list '())
                                (result post-data))
                            (maphash (lambda (emoji posts)
                                       (push `((emoji . ,emoji) (posts . ,posts))
                                             moods-list))
                                     moods-by-emoji)
                            ;; Add reactions if any
                            (when moods-list
                              (setq result (append result `((reactions . ,moods-list)))))
                            ;; Add boosts if any
                            (when (and boosts-list (> (length boosts-list) 0))
                              (setq result (append result `((boosts . ,boosts-list)))))
                            result))
                      post-data))
                (progn (kill-buffer buffer) post-data)))
          post-data))
    ;; No relay configured
    post-data))

(defun org-social-ui--display-thread-tree (replies-tree)
  "Display REPLIES-TREE structure from relay.
Each element in REPLIES-TREE is an alist with \\='post, \\='children, and \\='moods keys."
  (dolist (reply-node replies-tree)
    (let ((post-url (cdr (assoc 'post reply-node)))
          (children (cdr (assoc 'children reply-node)))
          (moods (cdr (assoc 'moods reply-node))))
      ;; Fetch and display the reply post
      (when post-url
        (let ((post-data (org-social-ui--fetch-post-sync post-url)))
          (when post-data
            ;; Add reactions (moods) from Relay to post data
            (when moods
              (setq post-data (append post-data `((reactions . ,moods)))))
            ;; Filter out simple votes (poll_option without text content)
            (let ((poll-option (alist-get 'poll_option post-data))
                  (text (alist-get 'text post-data)))
              (when (or (not poll-option)
                        (and poll-option text (not (string-empty-p (string-trim text)))))
                ;; Only render if not a simple vote
                (org-social-ui--post-component post-data nil))))))
      ;; Recursively display children (if any)
      (when (and children (> (length children) 0))
        (org-social-ui--display-thread-tree (if (vectorp children)
                                                (append children nil)
                                              children))))))

(defun org-social-ui--switch-to-timeline ()
  "Switch to timeline buffer if it exists, otherwise load timeline."
  (interactive)
  (let ((timeline-buffer (get-buffer org-social-ui--timeline-buffer-name)))
    (if timeline-buffer
        ;; Buffer exists, just switch to it
        (switch-to-buffer timeline-buffer)
      ;; Buffer doesn't exist, load timeline
      (org-social-ui-timeline))))

(defun org-social-ui--thread-go-back ()
  "Go back to previous thread level or timeline and kill current buffer."
  (interactive)
  (let ((current-buffer (current-buffer)))
    (if (> (length org-social-ui--thread-stack) 1)
        (progn
          ;; Remove current level from stack
          (pop org-social-ui--thread-stack)
          (setq org-social-ui--thread-level (length org-social-ui--thread-stack))
          ;; Navigate to previous thread level (which is already in the stack)
          (let ((parent-post-url (car org-social-ui--thread-stack)))
            ;; Remove it temporarily to avoid duplicating when org-social-ui-thread pushes
            (pop org-social-ui--thread-stack)
            (setq org-social-ui--thread-level (length org-social-ui--thread-stack))
            ;; Now navigate (this will push it back)
            (org-social-ui-thread parent-post-url)))
      ;; If at top level, go back to timeline and clear stack
      (setq org-social-ui--thread-stack nil)
      (setq org-social-ui--thread-level 0)
      (org-social-ui--switch-to-timeline))
    ;; Always kill the buffer we came from
    (when (buffer-live-p current-buffer)
      (kill-buffer current-buffer))))

(provide 'org-social-ui-utils)
;;; org-social-ui-utils.el ends here
