(ns ajet.chat.tui-sm.presence
  "Presence batching for the TUI session manager.

   Collects presence events over configurable windows (default 60s),
   then diffs against the previous state and sends only changes as
   presence.update SSE events to connected clients.

   Typing indicators are forwarded immediately (not batched)."
  (:require [clojure.tools.logging :as log])
  (:import [java.time Instant]))

;;; ---------------------------------------------------------------------------
;;; State
;;; ---------------------------------------------------------------------------

;; Current window accumulator: {community-id {user-id {:status "online" :last-seen instant}}}
(defonce ^:private current-window (atom {}))

;; Previous flush snapshot: same shape as current-window
(defonce ^:private previous-snapshot (atom {}))

;; The scheduled flush future
(defonce ^:private flush-future (atom nil))

;;; ---------------------------------------------------------------------------
;;; Accumulate
;;; ---------------------------------------------------------------------------

(defn record-presence!
  "Record a presence event into the current batch window.

   community-id: string UUID of the community
   user-id: string UUID of the user
   status: string like \"online\", \"idle\", \"offline\""
  [community-id user-id status]
  (swap! current-window assoc-in [community-id user-id]
         {:status    status
          :last-seen (.toString (Instant/now))}))

;;; ---------------------------------------------------------------------------
;;; Diff & Flush
;;; ---------------------------------------------------------------------------

(defn- diff-presence
  "Compare current and previous presence maps. Returns a map of
   {community-id [{:user-id ... :status ... :last-seen ...}]} containing
   only entries that changed."
  [current previous]
  (reduce-kv
    (fn [acc community-id users]
      (let [prev-users (get previous community-id {})
            changes    (reduce-kv
                         (fn [ch user-id state]
                           (let [prev-state (get prev-users user-id)]
                             (if (= state prev-state)
                               ch
                               (conj ch (assoc state :user-id user-id)))))
                         []
                         users)]
        (if (seq changes)
          (assoc acc community-id changes)
          acc)))
    {}
    current))

(defn flush!
  "Flush the current presence window. Computes diff against the previous
   snapshot and returns the changes. Resets the window for the next cycle.

   Returns: {community-id [{:user-id ... :status ... :last-seen ...}]}
   or nil if no changes."
  []
  (let [current  @current-window
        previous @previous-snapshot
        changes  (diff-presence current previous)]
    (reset! previous-snapshot current)
    (reset! current-window {})
    (when (seq changes)
      changes)))

;;; ---------------------------------------------------------------------------
;;; Scheduled Flush
;;; ---------------------------------------------------------------------------

(defn start-flush-scheduler!
  "Start a background thread that flushes presence every interval-ms
   milliseconds and calls broadcast-fn with the changes.

   broadcast-fn: (fn [changes]) where changes is the diff map from flush!
   interval-ms: flush interval in milliseconds (default 60000)

   Returns: the future running the scheduler."
  [broadcast-fn interval-ms]
  (let [running (volatile! true)
        f (future
            (log/info "Presence flush scheduler started, interval:" interval-ms "ms")
            (while @running
              (try
                (Thread/sleep interval-ms)
                (when-let [changes (flush!)]
                  (try
                    (broadcast-fn changes)
                    (catch Exception e
                      (log/error e "Error broadcasting presence changes"))))
                (catch InterruptedException _
                  (vreset! running false))
                (catch Exception e
                  (log/error e "Error in presence flush loop"))))
            (log/info "Presence flush scheduler stopped"))]
    (reset! flush-future f)
    f))

(defn stop-flush-scheduler!
  "Stop the presence flush scheduler."
  []
  (when-let [f @flush-future]
    (future-cancel f)
    (reset! flush-future nil)
    (log/info "Presence flush scheduler cancelled")))

;;; ---------------------------------------------------------------------------
;;; Reset (for REPL)
;;; ---------------------------------------------------------------------------

(defn reset-state!
  "Reset all presence state. For use during REPL development."
  []
  (stop-flush-scheduler!)
  (reset! current-window {})
  (reset! previous-snapshot {}))
