(ns ajet.chat.tui-sm.sse
  "SSE endpoint for TUI clients.

   Uses http-kit's with-channel for async streaming. Each connected client
   gets a dedicated SSE stream with JSON-structured events.

   Connection lifecycle:
     1. Client connects to GET /tui/sse/events
     2. Server builds init event (fetches communities, channels, DMs, user from API)
     3. Server subscribes to relevant NATS subjects
     4. Events flow: NATS -> convert to SSE event type -> send to client
     5. On disconnect: unsubscribe NATS, remove from connection tracker

   Connection tracking:
     {user-id {:sse-channel   ch
               :active-community uuid
               :active-channel   uuid
               :nats-subs       [dispatchers]
               :last-event-id   int
               :connected-at    instant}}"
  (:require [org.httpkit.server :as http-kit]
            [clojure.data.json :as json]
            [clojure.tools.logging :as log]
            [ajet.chat.shared.api-client :as api]
            [ajet.chat.shared.eventbus :as eventbus]
            [ajet.chat.tui-sm.presence :as presence])
  (:import [java.time Instant]))

;;; ---------------------------------------------------------------------------
;;; Connection Tracker
;;; ---------------------------------------------------------------------------

;; Atom holding all active connections: {user-id conn-info-map}
(defonce connections (atom {}))

(defn connection-count
  "Return the current number of active SSE connections."
  []
  (count @connections))

(defn get-connection
  "Retrieve connection info for a user."
  [user-id]
  (get @connections user-id))

(defn- update-connection!
  "Update a field in a user's connection tracking entry."
  [user-id k v]
  (swap! connections assoc-in [user-id k] v))

;;; ---------------------------------------------------------------------------
;;; SSE Wire Format
;;; ---------------------------------------------------------------------------

(defn- format-sse-event
  "Format a single SSE event as a string.
   Returns: \"event: <type>\\nid: <id>\\ndata: <json>\\n\\n\""
  [event-type event-id data]
  (str "event: " event-type "\n"
       "id: " event-id "\n"
       "data: " (json/write-str data) "\n\n"))

(defn- next-event-id!
  "Atomically increment and return the next event ID for a connection."
  [user-id]
  (let [result (swap! connections update-in [user-id :last-event-id] (fnil inc 0))]
    (get-in result [user-id :last-event-id])))

(defn send-event!
  "Send an SSE event to a specific user's connection.

   Returns true if sent, false if the connection is gone or buffer exceeded."
  [user-id event-type data]
  (when-let [conn (get-connection user-id)]
    (let [ch (:sse-channel conn)]
      (when (and ch (http-kit/open? ch))
        (let [event-id (next-event-id! user-id)
              payload  (format-sse-event event-type event-id data)]
          (http-kit/send! ch payload false)
          true)))))

(defn send-ping!
  "Send a keepalive ping event to a user's connection."
  [user-id]
  (send-event! user-id "ping" {}))

;;; ---------------------------------------------------------------------------
;;; NATS Event Mapping
;;; ---------------------------------------------------------------------------

(def ^:private nats-type->sse-type
  "Map NATS event :type keywords to SSE event type strings."
  {:message/created    "message.new"
   :message/updated    "message.edit"
   :message/deleted    "message.delete"
   :reaction/added     "reaction.add"
   :reaction/removed   "reaction.remove"
   :typing/started     "typing.start"
   :typing/stopped     "typing.stop"
   :presence/updated   "presence.update"
   :channel/created    "channel.new"
   :channel/updated    "channel.update"
   :channel/deleted    "channel.delete"
   :member/joined      "member.join"
   :member/left        "member.leave"
   :notification/new   "notification"
   :unread/updated     "unread.update"})

(defn- nats-event->sse
  "Convert a NATS event envelope to [sse-event-type data-map].
   Returns nil if the event type is unknown."
  [nats-event]
  (let [event-type (:type nats-event)
        sse-type   (get nats-type->sse-type event-type)]
    (when sse-type
      [sse-type (:payload nats-event)])))

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

(defn- make-nats-handler
  "Create a NATS message handler that converts events and sends to the SSE client.
   Implements backpressure: disconnects client if buffer exceeds limit."
  [user-id backpressure-limit]
  (fn [nats-event]
    (try
      (let [conn (get-connection user-id)]
        ;; Check backpressure -- disconnect if exceeded
        (if (and conn (> (:last-event-id conn 0) backpressure-limit))
          (do
            (log/warn "Backpressure limit exceeded for user" user-id
                      "- disconnecting client")
            (when-let [ch (:sse-channel conn)]
              (http-kit/close ch)))

          ;; Normal event processing
          (do
            ;; Accumulate presence events for batching
            (when (= :presence/updated (:type nats-event))
              (let [payload (:payload nats-event)]
                (presence/record-presence!
                  (:community-id payload)
                  (:user-id payload)
                  (:status payload))))

            ;; Convert and send (skip presence -- those are batched)
            (when (not= :presence/updated (:type nats-event))
              (when-let [[sse-type data] (nats-event->sse nats-event)]
                (send-event! user-id sse-type data))))))
      (catch Exception e
        (log/error e "Error processing NATS event for user" user-id)))))

(defn- subscribe-user!
  "Subscribe a user to their relevant NATS subjects.
   Returns a vector of dispatcher handles."
  [nats-conn user-id community-id backpressure-limit]
  (let [handler      (make-nats-handler user-id backpressure-limit)
        community-sub (when community-id
                        (eventbus/subscribe!
                          nats-conn
                          (str "chat.events." community-id)
                          handler))
        presence-sub  (when community-id
                        (eventbus/subscribe!
                          nats-conn
                          (str "chat.presence." community-id)
                          handler))
        typing-sub    (when community-id
                        (eventbus/subscribe!
                          nats-conn
                          (str "chat.typing." community-id ".>")
                          handler))
        notif-sub     (eventbus/subscribe!
                        nats-conn
                        (str "chat.notifications." user-id)
                        handler)]
    (filterv some? [community-sub presence-sub typing-sub notif-sub])))

(defn- unsubscribe-all!
  "Unsubscribe from all NATS dispatchers for a connection."
  [nats-conn dispatchers]
  (doseq [d dispatchers]
    (try
      (eventbus/unsubscribe! nats-conn d)
      (catch Exception e
        (log/error e "Error unsubscribing NATS dispatcher")))))

;;; ---------------------------------------------------------------------------
;;; Init Event
;;; ---------------------------------------------------------------------------

(defn- build-init-event
  "Fetch initial data from the API and build the init event payload.
   Returns a map with communities, channels, dms, user, and unread counts."
  [ctx]
  (let [user         (api/get-me ctx)
        communities  (api/get-communities ctx)
        ;; For each community, fetch channels
        comm-channels (reduce
                        (fn [acc comm]
                          (let [cid (or (:id comm) (:community_id comm))]
                            (try
                              (assoc acc (str cid) (api/get-channels ctx cid))
                              (catch Exception e
                                (log/warn "Failed to fetch channels for community"
                                          cid (ex-message e))
                                acc))))
                        {}
                        communities)
        dms          (api/get-dms ctx)
        unread       (try (api/get-unread-count ctx)
                       (catch Exception _e {:count 0}))]
    {:user        user
     :communities communities
     :channels    comm-channels
     :dms         dms
     :unread      unread
     :server-time (.toString (Instant/now))}))

;;; ---------------------------------------------------------------------------
;;; Resubscription (for navigate)
;;; ---------------------------------------------------------------------------

(defn resubscribe!
  "Unsubscribe from old NATS subjects and subscribe to new ones for the
   user's active community. Called when the user navigates to a different
   community."
  [nats-conn user-id new-community-id backpressure-limit]
  (let [conn (get-connection user-id)]
    (when conn
      ;; Unsubscribe old
      (unsubscribe-all! nats-conn (:nats-subs conn))
      ;; Subscribe new
      (let [new-subs (subscribe-user! nats-conn user-id new-community-id
                                      backpressure-limit)]
        (swap! connections update user-id assoc
               :nats-subs        new-subs
               :active-community new-community-id)
        new-subs))))

;;; ---------------------------------------------------------------------------
;;; Disconnect
;;; ---------------------------------------------------------------------------

(defn- disconnect!
  "Clean up a user's connection: unsubscribe NATS, remove from tracker."
  [nats-conn user-id]
  (when-let [conn (get-connection user-id)]
    (log/info "Disconnecting user" user-id)
    (unsubscribe-all! nats-conn (:nats-subs conn))
    (swap! connections dissoc user-id)))

;;; ---------------------------------------------------------------------------
;;; SSE Endpoint Handler
;;; ---------------------------------------------------------------------------

(defn sse-handler
  "Ring handler for GET /tui/sse/events.

   Opens an SSE stream via http-kit's with-channel. On connect:
     1. Checks max connections
     2. Builds init event from API
     3. Subscribes to NATS
     4. Sends init event

   On close: unsubscribes NATS, removes from tracker."
  [request]
  (let [system       (:system request)
        nats-conn    (:nats system)
        config       (:config system)
        max-conns    (get-in config [:server :max-connections] 5000)
        bp-limit     (get-in config [:sse :backpressure-limit] 1000)
        api-base-url (get-in config [:api :base-url] "http://localhost:3001")
        user-id      (:user-id request)]
    (if-not user-id
      {:status  401
       :headers {"Content-Type" "application/json"}
       :body    (json/write-str {:error {:code "UNAUTHORIZED"
                                         :message "Missing X-User-Id header"}})}
      (if (>= (connection-count) max-conns)
        {:status  503
         :headers {"Content-Type" "application/json"}
         :body    (json/write-str {:error {:code "SERVICE_UNAVAILABLE"
                                           :message "Maximum connections exceeded"}})}
        (http-kit/with-channel request ch
          (if (not (http-kit/open? ch))
            (log/warn "Channel closed immediately for user" user-id)
            (do
              (log/info "SSE connection opened for user" user-id)

              ;; Set SSE headers
              (http-kit/send! ch
                {:status  200
                 :headers {"Content-Type"  "text/event-stream"
                           "Cache-Control" "no-cache"
                           "Connection"    "keep-alive"
                           "X-Accel-Buffering" "no"}}
                false)

              ;; Build context for API calls
              (let [ctx (api/request->ctx request api-base-url)]

                ;; Register on-close before doing anything async
                (http-kit/on-close ch
                  (fn [_status]
                    (log/info "SSE connection closed for user" user-id)
                    (disconnect! nats-conn user-id)))

                ;; Build init event
                (let [init-data (try
                                  (build-init-event ctx)
                                  (catch Exception e
                                    (log/error e "Failed to build init event for user"
                                               user-id)
                                    {:error "Failed to load initial data"
                                     :server-time (.toString (Instant/now))}))]

                  ;; Track the connection
                  (swap! connections assoc user-id
                         {:sse-channel      ch
                          :active-community nil
                          :active-channel   nil
                          :nats-subs        []
                          :last-event-id    0
                          :connected-at     (.toString (Instant/now))})

                  ;; Send init event
                  (let [event-id (next-event-id! user-id)]
                    (http-kit/send! ch
                      (format-sse-event "init" event-id init-data)
                      false))

                  ;; Subscribe to user-level NATS subjects (notifications)
                  ;; Community-level subs happen when user navigates
                  (let [notif-handler (make-nats-handler user-id bp-limit)
                        notif-sub    (eventbus/subscribe!
                                       nats-conn
                                       (str "chat.notifications." user-id)
                                       notif-handler)]
                    (update-connection! user-id :nats-subs [notif-sub])))))))))))

;;; ---------------------------------------------------------------------------
;;; Broadcast Helpers
;;; ---------------------------------------------------------------------------

(defn broadcast-to-community!
  "Send an SSE event to all users currently viewing a given community."
  [community-id event-type data]
  (doseq [[user-id conn] @connections]
    (when (= (str community-id) (str (:active-community conn)))
      (send-event! user-id event-type data))))

(defn broadcast-presence-changes!
  "Broadcast batched presence changes to connected clients.
   Called by the presence flush scheduler.

   changes: {community-id [{:user-id ... :status ... :last-seen ...}]}"
  [changes]
  (doseq [[community-id user-changes] changes]
    (broadcast-to-community! community-id "presence.update"
                             {:community-id community-id
                              :users        user-changes})))

;;; ---------------------------------------------------------------------------
;;; Ping All
;;; ---------------------------------------------------------------------------

(defn ping-all!
  "Send a keepalive ping to every connected client."
  []
  (doseq [[user-id _conn] @connections]
    (try
      (send-ping! user-id)
      (catch Exception e
        (log/error e "Error sending ping to user" user-id)))))

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

(defn reset-connections!
  "Close all connections and clear tracking state."
  [nats-conn]
  (doseq [[user-id _conn] @connections]
    (disconnect! nats-conn user-id))
  (reset! connections {}))
