(ns ajet.chat.web.sse
  "SSE/Datastar handler for real-time browser updates.

   Uses http-kit's with-channel for async SSE streaming.
   On connect: subscribes to NATS subjects for the user's communities, DMs, notifications.
   On NATS event: renders a Hiccup fragment and sends as Datastar SSE event.
   On disconnect: unsubscribes from NATS and cleans up connection state.

   Connection tracking atom shape:
     {user-id {:sse-channel   <http-kit-channel>
               :active-community  \"uuid\"
               :active-channel    \"uuid\"
               :nats-subs    [dispatcher1 dispatcher2 ...]
               :last-seen    <Instant>}}"
  (:require [org.httpkit.server :as hk]
            [clojure.tools.logging :as log]
            [clojure.string :as str]
            [hiccup2.core :as h]
            [ajet.chat.shared.api-client :as api]
            [ajet.chat.shared.eventbus :as eventbus]
            [ajet.chat.shared.markdown :as markdown]
            [ajet.chat.shared.mentions :as mentions]
            [ajet.chat.web.components :as c])
  (:import [java.time Instant]))

;;; ---------------------------------------------------------------------------
;;; Datastar SSE Protocol
;;; ---------------------------------------------------------------------------

(defn- sse-event
  "Format a Datastar SSE event string.
   event-type: :patch-elements, :remove-element, :patch-signals, :execute-script
   data-lines: map of data line key/value pairs.

   Datastar SSE format:
     event: datastar-patch-elements
     data: selector #target-id
     data: mode outer
     data: elements <div>...</div>"
  [event-type data-lines]
  (let [event-name (case event-type
                     :patch-elements  "datastar-patch-elements"
                     :remove-element  "datastar-patch-elements"
                     :patch-signals   "datastar-patch-signals"
                     :execute-script  "datastar-execute-script"
                     (str "datastar-" (name event-type)))]
    (str "event: " event-name "\n"
         (apply str
           (for [[k v] data-lines]
             (str "data: " (name k) " " v "\n")))
         "\n")))

(defn- send-sse!
  "Send an SSE event to an http-kit channel. Returns false if channel is closed."
  [ch event-str]
  (try
    (hk/send! ch event-str false)
    true
    (catch Exception e
      (log/debug "SSE send failed:" (ex-message e))
      false)))

(defn- send-patch!
  "Send a Datastar patch-elements event with rendered Hiccup."
  ([ch hiccup-fragment]
   (send-patch! ch hiccup-fragment {}))
  ([ch hiccup-fragment {:keys [selector mode]
                        :or   {mode "outer"}}]
   (let [html-str (str/replace (str (h/html hiccup-fragment)) "\n" " ")
         lines    (cond-> []
                    selector (conj [:selector selector])
                    true     (conj [:mode mode])
                    true     (conj [:elements html-str]))]
     (send-sse! ch (sse-event :patch-elements lines)))))

(defn- send-append!
  "Send a Datastar patch-elements event in append mode."
  [ch selector hiccup-fragment]
  (let [html-str (str/replace (str (h/html hiccup-fragment)) "\n" " ")]
    (send-sse! ch (sse-event :patch-elements
                    [[:selector selector]
                     [:mode "append"]
                     [:elements html-str]]))))

(defn- send-remove!
  "Send a Datastar remove event."
  [ch selector]
  (send-sse! ch (sse-event :remove-element
                  [[:selector selector]
                   [:mode "remove"]
                   [:elements "<div></div>"]])))

(defn- send-signals!
  "Send a Datastar patch-signals event."
  [ch signals-json]
  (send-sse! ch (sse-event :patch-signals
                  [[:signals signals-json]])))

(defn- send-script!
  "Send a Datastar execute-script event."
  [ch script]
  (send-sse! ch (sse-event :execute-script
                  [[:script script]])))

;;; ---------------------------------------------------------------------------
;;; NATS Event Handlers
;;; ---------------------------------------------------------------------------

(defn- handle-message-created
  "New message arrived -- append to message list."
  [ch event user]
  (let [payload (:payload event)
        msg     payload]
    (send-append! ch "#messages-container"
      (c/message-component msg user))))

(defn- handle-message-updated
  "Message edited -- replace the message element."
  [ch event user]
  (let [payload (:payload event)
        msg     payload
        msg-id  (str (:id msg))]
    (send-patch! ch
      (c/message-component msg user)
      {:selector (str "#msg-" msg-id)})))

(defn- handle-message-deleted
  "Message deleted -- remove from DOM."
  [ch event _user]
  (let [msg-id (str (get-in event [:payload :id]))]
    (send-remove! ch (str "#msg-" msg-id))))

(defn- handle-reaction-updated
  "Reaction added/removed -- replace the reactions bar on the message."
  [ch event _user]
  (let [payload   (:payload event)
        msg-id    (str (:message-id payload (:message_id payload)))
        reactions (or (:reactions payload) [])]
    (if (seq reactions)
      (send-patch! ch
        [:div {:id    (str "reactions-" msg-id)
               :class "flex flex-wrap gap-1 mt-1"}
         (for [r reactions]
           [:button {:key   (str (:emoji r))
                     :class "flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-surface0 border border-surface1 hover:border-blue transition-colors"
                     "data-on:click" (str "@post('/web/reactions', {headers: {'X-Message-Id': '" msg-id "', 'X-Emoji': '" (:emoji r) "'}})")}
            [:span (:emoji r)]
            [:span {:class "text-subtext0"} (count (:users r))]])]
        {:selector (str "#reactions-" msg-id)})
      (send-remove! ch (str "#reactions-" msg-id)))))

(defn- handle-typing
  "Typing indicator -- update the typing area."
  [ch event _user]
  (let [payload  (:payload event)
        username (or (:username payload) (:display-name payload) "Someone")
        user-id  (str (:user-id payload (:user_id payload)))]
    (send-patch! ch
      [:div {:id "typing-indicator" :class "px-4 py-1 text-xs text-overlay0 h-6"}
       [:span (str username " is typing...")]]
      {:selector "#typing-indicator"})
    ;; Clear typing indicator after 3 seconds via script
    (send-script! ch
      (str "setTimeout(() => {"
           "  let el = document.getElementById('typing-indicator');"
           "  if(el) el.innerHTML = '';"
           "}, 3000)"))))

(defn- handle-presence-update
  "User presence changed -- update the presence dot."
  [ch event _user]
  (let [payload (:payload event)
        uid     (str (:user-id payload (:user_id payload)))
        online  (:online payload true)]
    (send-patch! ch
      [:span {:id    (str "presence-" uid)
              :class (str "ml-auto w-2 h-2 rounded-full "
                          (if online "bg-green" "bg-overlay0")
                          (when-not online " hidden"))}]
      {:selector (str "#presence-" uid)})))

(defn- handle-channel-created
  "New channel created -- append to sidebar."
  [ch event _user]
  (let [payload (:payload event)
        channel payload
        cid     (str (:id channel))
        ch-type (or (:type channel) "text")
        prefix  (if (= ch-type "voice") "\uD83D\uDD0A" "#")]
    (send-append! ch "#channel-list"
      [:div {:id    (str "sidebar-channel-" cid)
             :class "flex items-center px-2 py-1 rounded cursor-pointer text-sm text-overlay1 hover:text-subtext1 hover:bg-hover"
             "data-on:click" (str "$navChannel = '" cid "'; $navCommunity = ''; $navTarget = ''; @post('/web/navigate')")}
       [:span {:class "mr-1.5 text-overlay0 text-xs"} prefix]
       [:span {:class "truncate"} (:name channel)]
       [:span {:id    (str "unread-badge-" cid)
               :class "ml-auto hidden bg-red text-base text-xs font-bold rounded-full px-1.5 min-w-[1.25rem] text-center"}]])))

(defn- handle-unread-count
  "Unread count changed -- update badge on sidebar channel."
  [ch event _user]
  (let [payload    (:payload event)
        channel-id (str (:channel-id payload (:channel_id payload)))
        count      (:count payload 0)]
    (if (pos? count)
      (send-patch! ch
        [:span {:id    (str "unread-badge-" channel-id)
                :class "ml-auto bg-red text-base text-xs font-bold rounded-full px-1.5 min-w-[1.25rem] text-center"}
         (str count)]
        {:selector (str "#unread-badge-" channel-id)})
      (send-patch! ch
        [:span {:id    (str "unread-badge-" channel-id)
                :class "ml-auto hidden bg-red text-base text-xs font-bold rounded-full px-1.5 min-w-[1.25rem] text-center"}]
        {:selector (str "#unread-badge-" channel-id)}))))

(defn- handle-notification
  "Notification received -- update notification badge and show toast."
  [ch event _user]
  (let [payload (:payload event)
        n-type  (keyword (or (:notification-type payload) (:type payload) "mention"))
        count   (:unread-count payload 1)]
    ;; Update notification badge
    (when (pos? count)
      (send-patch! ch
        [:span {:id "notification-badge"
                :class "absolute -top-1 -right-1 bg-red text-base text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center"}
         (if (> count 99) "99+" (str count))]
        {:selector "#notification-badge"})
      (send-signals! ch (str "{unreadCount: " count "}")))
    ;; Show toast
    (send-append! ch "#toast-container"
      (c/notification-toast {:id    (str (java.util.UUID/randomUUID))
                             :title (case n-type
                                      :mention "New Mention"
                                      :dm      "New Direct Message"
                                      :thread-reply "Thread Reply"
                                      :invite  "Invitation"
                                      "Notification")
                             :body  (or (:preview payload) (:body payload) "")
                             :type  n-type}))
    ;; Auto-dismiss toast after 5 seconds
    (send-script! ch
      "setTimeout(() => { let t = document.querySelector('#toast-container > div:first-child'); if(t) t.remove(); }, 5000)")))

;;; ---------------------------------------------------------------------------
;;; NATS Event Router
;;; ---------------------------------------------------------------------------

(defn- route-event
  "Route a NATS event to the appropriate handler based on event type."
  [ch event user]
  (let [event-type (keyword (or (:type event) "unknown"))]
    (log/debug "SSE routing event:" event-type)
    (case event-type
      :message/created       (handle-message-created ch event user)
      :message/updated       (handle-message-updated ch event user)
      :message/deleted       (handle-message-deleted ch event user)
      :reaction/added        (handle-reaction-updated ch event user)
      :reaction/removed      (handle-reaction-updated ch event user)
      :typing/started        (handle-typing ch event user)
      :presence/updated      (handle-presence-update ch event user)
      :channel/created       (handle-channel-created ch event user)
      :unread/updated        (handle-unread-count ch event user)
      :notification/created  (handle-notification ch event user)
      (log/debug "Unhandled NATS event type:" event-type))))

;;; ---------------------------------------------------------------------------
;;; NATS Subscription Management
;;; ---------------------------------------------------------------------------

(defn- subscribe-user!
  "Subscribe to all relevant NATS subjects for a user.
   Returns a vector of Dispatcher objects."
  [nats sse-ch user conn-state]
  (let [user-id      (:id user)
        community-id (:active-community conn-state)
        channel-id   (:active-channel conn-state)
        handler      (fn [event] (route-event sse-ch event user))
        subs         (atom [])]

    ;; Community events (messages, channels, reactions, etc.)
    (when community-id
      (let [d (eventbus/subscribe! nats
                (str "chat.events." community-id)
                handler)]
        (swap! subs conj d)))

    ;; Typing indicators for the active channel
    (when (and community-id channel-id)
      (let [d (eventbus/subscribe! nats
                (str "chat.typing." community-id "." channel-id)
                handler)]
        (swap! subs conj d)))

    ;; Presence updates for the community
    (when community-id
      (let [d (eventbus/subscribe! nats
                (str "chat.presence." community-id)
                handler)]
        (swap! subs conj d)))

    ;; Per-user notifications
    (when user-id
      (let [d (eventbus/subscribe! nats
                (str "chat.notifications." user-id)
                handler)]
        (swap! subs conj d)))

    ;; DM channels -- subscribe to each DM the user is in
    ;; We subscribe to a wildcard for DMs since the user could have many
    (when user-id
      (let [d (eventbus/subscribe! nats
                "chat.dm.*"
                (fn [event]
                  ;; Filter: only route events for DM channels this user is in
                  (let [payload (:payload event)
                        participants (or (:participants payload) (:participant-ids payload) [])]
                    (when (or (empty? participants)
                              (some #{(str user-id)} (map str participants)))
                      (handler event)))))]
        (swap! subs conj d)))

    @subs))

(defn- unsubscribe-all!
  "Unsubscribe from all NATS dispatchers."
  [nats dispatchers]
  (doseq [d dispatchers]
    (try
      (eventbus/unsubscribe! nats d)
      (catch Exception e
        (log/debug "Error unsubscribing NATS dispatcher:" (ex-message e))))))

;;; ---------------------------------------------------------------------------
;;; Connection Lifecycle
;;; ---------------------------------------------------------------------------

(defn- on-sse-connect!
  "Handle new SSE connection. Fetch user info, subscribe to NATS, track state."
  [connections nats api-url request sse-ch]
  (let [headers  (:headers request)
        user-id  (get headers "x-user-id")
        ctx      (api/request->ctx request api-url)]
    (when user-id
      (try
        (let [user         (api/get-me ctx)
              communities  (api/get-communities ctx)
              community    (first communities)
              community-id (when community (str (:id community)))
              channels     (when community-id (api/get-channels ctx community-id))
              channel      (first (filter #(= "text" (name (or (:type %) :text))) channels))
              channel-id   (when channel (str (:id channel)))

              conn-state   {:active-community community-id
                            :active-channel   channel-id}
              nats-subs    (subscribe-user! nats sse-ch user conn-state)]

          ;; Store connection state
          (swap! connections assoc user-id
                 {:sse-channel       sse-ch
                  :active-community  community-id
                  :active-channel    channel-id
                  :nats-subs         nats-subs
                  :last-seen         (Instant/now)
                  :user              user
                  :api-ctx           ctx})

          (log/info "SSE connected for user" user-id
                    "community" community-id "channel" channel-id)

          ;; Send initial heartbeat / keep-alive comment
          (send-sse! sse-ch ": connected\n\n"))
        (catch Exception e
          (log/error e "Error setting up SSE connection for user" user-id)
          (hk/close sse-ch))))))

(defn- on-sse-disconnect!
  "Handle SSE disconnection. Unsubscribe from NATS and clean up."
  [connections nats request _status]
  (let [user-id (get-in request [:headers "x-user-id"])]
    (when user-id
      (when-let [conn-state (get @connections user-id)]
        (unsubscribe-all! nats (:nats-subs conn-state))
        (swap! connections dissoc user-id)
        (log/info "SSE disconnected for user" user-id)))))

;;; ---------------------------------------------------------------------------
;;; Public API
;;; ---------------------------------------------------------------------------

(defn sse-handler
  "Ring handler for GET /sse/events.
   Establishes a persistent SSE connection using http-kit's with-channel."
  [request]
  (let [system      (:system request)
        nats        (:nats system)
        connections (:connections system)
        api-url     (:api-url system)]
    (hk/with-channel request ch
      ;; Set up SSE headers
      (hk/send! ch
        {:status  200
         :headers {"Content-Type"                "text/event-stream"
                   "Cache-Control"               "no-cache, no-store, must-revalidate"
                   "Connection"                  "keep-alive"
                   "X-Accel-Buffering"           "no"
                   "Access-Control-Allow-Origin"  "*"}}
        false)

      ;; On connection open, set up NATS subscriptions
      (on-sse-connect! connections nats api-url request ch)

      ;; On close, clean up
      (hk/on-close ch
        (fn [status]
          (on-sse-disconnect! connections nats request status))))))

(defn resubscribe!
  "Re-subscribe to NATS for a user after navigation (community/channel change).
   Called from handlers when the user navigates."
  [connections nats user-id new-community-id new-channel-id]
  (when-let [conn-state (get @connections user-id)]
    (let [sse-ch   (:sse-channel conn-state)
          user     (:user conn-state)
          old-subs (:nats-subs conn-state)]

      ;; Unsubscribe old
      (unsubscribe-all! nats old-subs)

      ;; Re-subscribe with new community/channel
      (let [new-state {:active-community new-community-id
                       :active-channel   new-channel-id}
            new-subs  (subscribe-user! nats sse-ch user new-state)]

        ;; Update connection state
        (swap! connections assoc user-id
               (merge conn-state
                      {:active-community new-community-id
                       :active-channel   new-channel-id
                       :nats-subs        new-subs
                       :last-seen        (Instant/now)}))

        (log/info "Re-subscribed user" user-id
                  "to community" new-community-id "channel" new-channel-id)))))

(defn disconnect-all!
  "Disconnect all SSE clients and clean up NATS subscriptions.
   Called during server shutdown."
  [connections nats]
  (doseq [[user-id conn-state] @connections]
    (try
      (unsubscribe-all! nats (:nats-subs conn-state))
      (when-let [ch (:sse-channel conn-state)]
        (hk/close ch))
      (catch Exception e
        (log/debug "Error disconnecting user" user-id ":" (ex-message e)))))
  (reset! connections {}))

(defn send-to-user!
  "Send a Datastar SSE event directly to a specific user's connection."
  [connections user-id event-type data-lines]
  (when-let [conn-state (get @connections user-id)]
    (when-let [ch (:sse-channel conn-state)]
      (send-sse! ch (sse-event event-type data-lines)))))

(defn send-fragment-to-user!
  "Send a Hiccup fragment to a specific user."
  [connections user-id hiccup-fragment & [{:keys [selector mode] :or {mode "outer"}}]]
  (when-let [conn-state (get @connections user-id)]
    (when-let [ch (:sse-channel conn-state)]
      (send-patch! ch hiccup-fragment {:selector selector :mode mode}))))
