(ns ajet.chat.web.handlers
  "Signal handlers for POST /web/* endpoints.

   Every handler:
   1. Extracts params from the Datastar request (signals + custom headers)
   2. Calls the API via shared api-client
   3. Returns a Datastar fragment response (or empty 204)

   All auth is handled by Auth GW upstream -- we receive X-User-Id, X-User-Role headers."
  (:require [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.web.components :as c]
            [ajet.chat.web.layout :as layout]
            [ajet.chat.web.sse :as sse]))

;;; ---------------------------------------------------------------------------
;;; Helpers
;;; ---------------------------------------------------------------------------

(defn build-api-ctx
  "Build an API client context from a Ring request and the system's API URL."
  [request]
  (let [api-url (get-in request [:system :api-url] "http://localhost:3001")]
    (api/request->ctx request api-url)))

(defn- get-signal
  "Extract a Datastar signal value from the request.
   Signals come as JSON body params or query params depending on GET/POST."
  [request signal-name]
  (or (get-in request [:params (keyword signal-name)])
      (get-in request [:body-params (keyword signal-name)])
      (get-in request [:params signal-name])))

(defn- get-header
  "Get a custom header value from the request."
  [request header-name]
  (get-in request [:headers (str/lower-case header-name)]))

(defn- fragment-event
  "Build a single datastar-patch-elements SSE event string for one fragment.
   Newlines in the rendered HTML are replaced with spaces to avoid breaking
   SSE data lines (each data: line must be a single line in the SSE protocol)."
  [hiccup-fragment & [{:keys [selector mode] :or {mode "outer"}}]]
  (let [html-str (str/replace (str (h/html hiccup-fragment)) "\n" " ")
        lines    (cond-> []
                   selector (conj (str "selector " selector))
                   true     (conj (str "mode " mode))
                   true     (conj (str "elements " html-str)))]
    (str "event: datastar-patch-elements\n"
         (apply str (map #(str "data: " % "\n") lines))
         "\n")))

(defn- datastar-fragment-response
  "Return an SSE response with a Datastar patch-elements event.
   Used for POST handlers that need to return a UI update."
  [hiccup-fragment & [{:keys [selector mode] :or {mode "outer"}}]]
  {:status  200
   :headers {"Content-Type"  "text/event-stream"
             "Cache-Control" "no-cache, no-store, must-revalidate"}
   :body    (fragment-event hiccup-fragment {:selector selector :mode mode})})

(defn- datastar-multi-fragment-response
  "Return an SSE response with multiple datastar-patch-elements events.
   Each fragment is morphed independently by its element id.
   Optionally includes a patch-signals event to sync client state."
  ([fragments]
   (datastar-multi-fragment-response fragments nil))
  ([fragments signals-json]
   {:status  200
    :headers {"Content-Type"  "text/event-stream"
              "Cache-Control" "no-cache, no-store, must-revalidate"}
    :body    (str (apply str (map fragment-event fragments))
                  (when signals-json
                    (str "event: datastar-patch-signals\n"
                         "data: signals " signals-json "\n"
                         "\n")))}))

(defn- datastar-signals-response
  "Return an SSE response with a Datastar patch-signals event."
  [signals-json]
  {:status  200
   :headers {"Content-Type"  "text/event-stream"
             "Cache-Control" "no-cache, no-store, must-revalidate"}
   :body    (str "event: datastar-patch-signals\n"
                 "data: signals " signals-json "\n"
                 "\n")})

(defn- datastar-remove-response
  "Return an SSE response that removes a DOM element."
  [selector]
  {:status  200
   :headers {"Content-Type"  "text/event-stream"
             "Cache-Control" "no-cache, no-store, must-revalidate"}
   :body    (str "event: datastar-patch-elements\n"
                 "data: selector " selector "\n"
                 "data: mode remove\n"
                 "data: elements <div></div>\n"
                 "\n")})

(defn- datastar-redirect-response
  "Return an SSE response that redirects the browser."
  [url]
  {:status  200
   :headers {"Content-Type"  "text/event-stream"
             "Cache-Control" "no-cache, no-store, must-revalidate"}
   :body    (str "event: datastar-execute-script\n"
                 "data: script window.location.href='" url "'\n"
                 "\n")})

(defn- empty-sse-response
  "Return an empty SSE response (no events, connection closes after)."
  []
  {:status  200
   :headers {"Content-Type"  "text/event-stream"
             "Cache-Control" "no-cache, no-store, must-revalidate"}
   :body    "\n"})

;;; ---------------------------------------------------------------------------
;;; Message Handlers
;;; ---------------------------------------------------------------------------

(defn send-message-handler
  "POST /web/messages -- send a new message.
   The message will appear via SSE push from NATS, so we return empty."
  [request]
  (let [ctx        (build-api-ctx request)
        channel-id (or (get-header request "x-channel-id")
                       (get-signal request "activeChannel"))
        parent-id  (get-header request "x-parent-id")
        body-text  (or (get-signal request "messageText")
                       (get-signal request "threadReply")
                       "")
        body-map   (cond-> {:body_md body-text}
                     parent-id (assoc :parent_id parent-id))]
    (when (and channel-id (not (str/blank? body-text)))
      (api/send-message ctx channel-id body-map))
    ;; Clear the input and return empty response
    (datastar-signals-response (if parent-id
                                 "{threadReply: ''}"
                                 "{messageText: ''}"))))

(defn edit-message-handler
  "POST /web/messages/:id/edit -- edit a message."
  [request]
  (let [ctx        (build-api-ctx request)
        message-id (get-in request [:path-params :id])
        body-text  (or (get-signal request "editText")
                       (get-signal request "messageText")
                       "")]
    (when (and message-id (not (str/blank? body-text)))
      (api/edit-message ctx message-id {:body_md body-text}))
    (empty-sse-response)))

(defn delete-message-handler
  "POST /web/messages/:id/delete -- delete a message."
  [request]
  (let [ctx        (build-api-ctx request)
        message-id (get-in request [:path-params :id])]
    (when message-id
      (api/delete-message ctx message-id))
    (empty-sse-response)))

;;; ---------------------------------------------------------------------------
;;; Load Older Messages Handler
;;; ---------------------------------------------------------------------------

(defn load-older-handler
  "POST /web/messages/older -- fetch older messages using before cursor.
   Reads oldestMessageId and activeChannel from Datastar signals.
   Prepends older messages into #messages-container and updates the cursor signal."
  [request]
  (let [ctx        (build-api-ctx request)
        channel-id (get-signal request "activeChannel")
        cursor     (get-signal request "oldestMessageId")
        user       (api/get-me ctx)]
    (if (or (str/blank? channel-id) (str/blank? cursor))
      (empty-sse-response)
      (let [messages (api/get-messages ctx channel-id
                       {:before cursor :limit 50})
            new-oldest (when (seq messages) (str (:id (first messages))))]
        (if (empty? messages)
          ;; No more messages — replace the load-older button with a notice
          (datastar-fragment-response
            [:div {:id "load-older-sentinel" :class "flex justify-center py-2"}
             [:span {:class "text-xs text-overlay0"} "Beginning of conversation"]])
          ;; Prepend older messages and update cursor
          (let [batch-html (str (h/html
                             [:div {:id (str "older-batch-" new-oldest)}
                              (for [msg messages]
                                (c/message-component msg user))]))
                html-str   (str/replace batch-html "\n" " ")]
            {:status  200
             :headers {"Content-Type"  "text/event-stream"
                       "Cache-Control" "no-cache, no-store, must-revalidate"}
             :body    (str "event: datastar-patch-elements\n"
                           "data: selector #messages-container\n"
                           "data: mode prepend\n"
                           "data: elements " html-str "\n"
                           "\n"
                           "event: datastar-patch-signals\n"
                           "data: signals {oldestMessageId: '" new-oldest "'}\n"
                           "\n")}))))))

;;; ---------------------------------------------------------------------------
;;; Reaction Handlers
;;; ---------------------------------------------------------------------------

(defn add-reaction-handler
  "POST /web/reactions -- add a reaction to a message."
  [request]
  (let [ctx        (build-api-ctx request)
        message-id (or (get-header request "x-message-id")
                       (get-signal request "threadMessageId"))
        emoji      (or (get-header request "x-emoji")
                       (get-signal request "emoji"))]
    (when (and message-id emoji)
      (api/add-reaction ctx message-id emoji))
    (empty-sse-response)))

(defn remove-reaction-handler
  "POST /web/reactions/remove -- remove a reaction from a message."
  [request]
  (let [ctx        (build-api-ctx request)
        message-id (or (get-header request "x-message-id")
                       (get-signal request "messageId"))
        emoji      (or (get-header request "x-emoji")
                       (get-signal request "emoji"))]
    (when (and message-id emoji)
      (api/remove-reaction ctx message-id emoji))
    (empty-sse-response)))

;;; ---------------------------------------------------------------------------
;;; Navigation Handler
;;; ---------------------------------------------------------------------------

(defn navigate-handler
  "POST /web/navigate -- switch community or channel.
   Updates connection tracking and re-subscribes NATS.
   Returns the updated sidebar + message list + channel header.
   Navigation intent is determined from explicit request headers only
   (not Datastar signals) to avoid stale state."
  [request]
  (let [ctx           (build-api-ctx request)
        user-id       (:user-id request)
        connections   (get-in request [:system :connections])
        nats          (get-in request [:system :nats])
        ;; Read navigation intent from dedicated nav signals (set right before @post)
        ;; These are distinct from activeCommunity/activeChannel to avoid stale state
        target        (let [v (get-signal request "navTarget")] (when-not (str/blank? v) v))
        community-id  (let [v (get-signal request "navCommunity")] (when-not (str/blank? v) v))
        channel-id    (let [v (get-signal request "navChannel")] (when-not (str/blank? v) v))]

    (cond
      ;; Navigate to DMs view
      (= target "dms")
      (let [communities (api/get-communities ctx)
            dms      (api/get-dms ctx)
            dm       (first dms)
            dm-id    (when dm (str (:id dm)))
            messages (when dm-id (api/get-messages ctx dm-id {:limit 50}))
            user     (api/get-me ctx)
            oldest   (when (seq messages) (str (:id (first messages))))]
        ;; Re-subscribe NATS (no community context for DMs)
        (when user-id
          (sse/resubscribe! connections nats user-id nil dm-id))
        ;; Return updated UI fragments — each matches an existing DOM element
        (datastar-multi-fragment-response
          [[:div {:id "community-strip"
                  :class "w-[72px] flex-shrink-0 bg-mantle flex flex-col items-center py-3 gap-2 overflow-y-auto"}
            (c/community-strip {:communities communities
                                :active-id   nil
                                :unread-count 0})]
           [:div {:id "sidebar"
                  :class "w-60 flex-shrink-0 bg-mantle flex flex-col border-r border-surface1"}
            (c/dm-sidebar {:dms dms :active-dm dm :user user})]
           [:div {:id "channel-header"}
            (c/channel-header (or dm {:name "Direct Messages"}))]
           [:div {:id "message-list"
                  :class "flex-1 overflow-y-auto px-4 py-2"}
            (c/message-list (or messages []) user)]
           [:div {:id "message-input-area" :class "px-4 pb-4"}
            (c/message-input dm)]]
          (str "{activeCommunity: '', activeChannel: '" (or dm-id "") "'"
               ", oldestMessageId: '" (or oldest "") "'}")))


      ;; Navigate to a specific channel
      channel-id
      (let [channel    (api/get-channel ctx channel-id)
            cid        (or community-id (str (:community-id channel)))
            community  (when cid (api/get-community ctx cid))
            channels   (when cid (api/get-channels ctx cid))
            messages   (api/get-messages ctx channel-id {:limit 50})
            categories (when cid (api/get-categories ctx cid))
            user       (api/get-me ctx)
            oldest     (when (seq messages) (str (:id (first messages))))]
        ;; Re-subscribe NATS
        (when user-id
          (sse/resubscribe! connections nats user-id cid channel-id))
        ;; Return updated UI fragments
        (datastar-multi-fragment-response
          [[:div {:id "sidebar"
                  :class "w-60 flex-shrink-0 bg-mantle flex flex-col border-r border-surface1"}
            (c/sidebar {:community  community
                        :channels   channels
                        :channel    channel
                        :categories categories
                        :user       user})]
           [:div {:id "channel-header"}
            (c/channel-header channel)]
           [:div {:id "message-list"
                  :class "flex-1 overflow-y-auto px-4 py-2"}
            (c/message-list (or messages []) user)]
           [:div {:id "message-input-area" :class "px-4 pb-4"}
            (c/message-input channel)]]
          (str "{activeCommunity: '" (or cid "") "', activeChannel: '" channel-id "'"
               ", oldestMessageId: '" (or oldest "") "'}")))


      ;; Navigate to a community (pick first channel)
      community-id
      (let [community  (api/get-community ctx community-id)
            channels   (api/get-channels ctx community-id)
            channel    (first (filter #(= "text" (name (or (:type %) :text))) channels))
            ch-id      (when channel (str (:id channel)))
            messages   (when ch-id (api/get-messages ctx ch-id {:limit 50}))
            categories (api/get-categories ctx community-id)
            user       (api/get-me ctx)
            oldest     (when (seq messages) (str (:id (first messages))))]
        ;; Re-subscribe NATS
        (when user-id
          (sse/resubscribe! connections nats user-id community-id ch-id))
        ;; Return updated UI
        (datastar-multi-fragment-response
          [[:div {:id "community-strip"
                  :class "w-[72px] flex-shrink-0 bg-mantle flex flex-col items-center py-3 gap-2 overflow-y-auto"}
            (c/community-strip {:communities (api/get-communities ctx)
                                :active-id   community-id
                                :unread-count 0})]
           [:div {:id "sidebar"
                  :class "w-60 flex-shrink-0 bg-mantle flex flex-col border-r border-surface1"}
            (c/sidebar {:community  community
                        :channels   channels
                        :channel    channel
                        :categories categories
                        :user       user})]
           [:div {:id "channel-header"}
            (c/channel-header channel)]
           [:div {:id "message-list"
                  :class "flex-1 overflow-y-auto px-4 py-2"}
            (c/message-list (or messages []) user)]
           [:div {:id "message-input-area" :class "px-4 pb-4"}
            (c/message-input channel)]]
          (str "{activeCommunity: '" community-id "', activeChannel: '" (or ch-id "") "'"
               ", oldestMessageId: '" (or oldest "") "'}")))


      :else
      (empty-sse-response))))

;;; ---------------------------------------------------------------------------
;;; Mark Read Handler
;;; ---------------------------------------------------------------------------

(defn mark-read-handler
  "POST /web/read -- mark a channel as read up to the latest message."
  [request]
  (let [ctx        (build-api-ctx request)
        channel-id (or (get-header request "x-channel-id")
                       (get-signal request "activeChannel"))
        message-id (or (get-header request "x-message-id")
                       (get-signal request "lastMessageId"))]
    (when (and channel-id message-id)
      (api/mark-read ctx channel-id {:last-read-message-id message-id}))
    (empty-sse-response)))

;;; ---------------------------------------------------------------------------
;;; Upload Handler
;;; ---------------------------------------------------------------------------

(defn upload-handler
  "POST /web/upload -- forward a multipart file upload to the API.
   Returns empty (the attachment will appear in the message via SSE)."
  [request]
  (let [ctx        (build-api-ctx request)
        channel-id (or (get-header request "x-channel-id")
                       (get-signal request "activeChannel"))
        file       (get-in request [:multipart-params "file"])
        tempfile   (:tempfile file)
        filename   (:filename file)
        ctype      (:content-type file)]
    (when (and channel-id tempfile)
      (api/upload-file ctx channel-id (str tempfile) (or ctype "image/png")))
    (empty-sse-response)))

;;; ---------------------------------------------------------------------------
;;; Typing Handler
;;; ---------------------------------------------------------------------------

(defn typing-handler
  "POST /web/typing -- publish a typing indicator directly to NATS (ephemeral).
   Bypasses the API for low latency."
  [request]
  (let [nats         (get-in request [:system :nats])
        user-id      (:user-id request)
        connections  (get-in request [:system :connections])
        conn-state   (get @connections user-id)
        community-id (or (get-header request "x-channel-id")
                         (:active-community conn-state))
        channel-id   (or (get-header request "x-channel-id")
                         (:active-channel conn-state))
        username     (or (get-in conn-state [:user :display-name])
                         (get-in conn-state [:user :username])
                         "Someone")]
    (when (and nats community-id channel-id)
      (eventbus/publish! nats
        (str "chat.typing." community-id "." channel-id)
        :typing/started
        {:user-id  user-id
         :username username
         :channel-id channel-id}))
    (empty-sse-response)))

;;; ---------------------------------------------------------------------------
;;; Search Handler
;;; ---------------------------------------------------------------------------

(defn search-handler
  "POST /web/search -- search messages via the API and return results fragment."
  [request]
  (let [ctx    (build-api-ctx request)
        query  (or (get-signal request "searchQuery") "")
        conn   (get @(get-in request [:system :connections]) (:user-id request))
        cid    (:active-community conn)]
    (if (str/blank? query)
      (datastar-fragment-response
        [:div {:id "search-results" :class "max-h-96 overflow-y-auto p-4"}
         [:div {:class "text-sm text-overlay0 text-center py-8"}
          "Type a query and press Enter to search."]]
        {:selector "#search-results"})
      (let [results (api/search ctx (cond-> {:q query}
                                      cid (assoc :community-id cid)))]
        (datastar-fragment-response
          [:div {:id "search-results" :class "max-h-96 overflow-y-auto p-4"}
           (c/search-results (or (:results results) results []))]
          {:selector "#search-results"})))))

;;; ---------------------------------------------------------------------------
;;; Slash Command Handler
;;; ---------------------------------------------------------------------------

(defn command-handler
  "POST /web/command -- execute a slash command via the API."
  [request]
  (let [ctx        (build-api-ctx request)
        text       (or (get-signal request "commandText")
                       (get-signal request "messageText") "")
        conn       (get @(get-in request [:system :connections]) (:user-id request))
        channel-id (:active-channel conn)
        community-id (:active-community conn)]
    (if (str/blank? text)
      (empty-sse-response)
      (let [;; Parse slash command: /command arg1 arg2
            parts    (str/split (str/trim text) #"\s+" 2)
            command  (str/replace (first parts) #"^/" "")
            args     (second parts)
            result   (api/execute-command ctx
                       (cond-> {:command command}
                         args         (assoc :args args)
                         channel-id   (assoc :channel-id channel-id)
                         community-id (assoc :community-id community-id)))]
        ;; Return the command result as a message-like fragment
        (if (:error result)
          (datastar-fragment-response
            [:div {:id "command-result"
                   :class "px-4 py-2 text-sm text-red bg-surface0 rounded mx-4 mb-2"}
             (str "Error: " (:message (:error result) "Command failed"))]
            {:selector "#message-input-area"
             :mode     "prepend"})
          (datastar-fragment-response
            [:div {:id "command-result"
                   :class "px-4 py-2 text-sm text-green bg-surface0 rounded mx-4 mb-2"}
             (or (:message result) (:body result) "Command executed.")]
            {:selector "#message-input-area"
             :mode     "prepend"}))))))

;;; ---------------------------------------------------------------------------
;;; Create Community Handler
;;; ---------------------------------------------------------------------------

(defn create-community-handler
  "POST /web/communities -- create a new community, then redirect to /app."
  [request]
  (let [ctx  (build-api-ctx request)
        name (or (get-signal request "communityName")
                 (get-in request [:params :name]) "")
        slug (or (get-signal request "communitySlug")
                 (get-in request [:params :slug]) "")]
    (if (or (str/blank? name) (str/blank? slug))
      (datastar-fragment-response
        [:div {:id "setup-error" :class "mt-4 text-red text-sm text-center"}
         "Please provide both a name and URL slug."]
        {:selector "#setup-error"})
      (try
        (api/create-community ctx {:name name :slug slug})
        (datastar-redirect-response "/app")
        (catch clojure.lang.ExceptionInfo e
          (let [data (ex-data e)
                msg  (or (get-in data [:body :error :message])
                         (ex-message e)
                         "Failed to create community")]
            (datastar-fragment-response
              [:div {:id "setup-error" :class "mt-4 text-red text-sm text-center"}
               msg]
              {:selector "#setup-error"})))))))

;;; ---------------------------------------------------------------------------
;;; Create Channel Handler
;;; ---------------------------------------------------------------------------

(defn create-channel-handler
  "POST /web/channels -- create a new channel in the active community."
  [request]
  (let [ctx          (build-api-ctx request)
        conn         (get @(get-in request [:system :connections]) (:user-id request))
        community-id (or (get-header request "x-community-id")
                         (:active-community conn))
        name         (or (get-signal request "channelName")
                         (get-in request [:params :name]) "")
        ch-type      (or (get-signal request "channelType") "text")
        visibility   (or (get-signal request "channelVisibility") "public")]
    (if (or (str/blank? community-id) (str/blank? name))
      (datastar-fragment-response
        [:div {:id "create-channel-error" :class "text-red text-sm"}
         "Please provide a channel name."])
      (try
        (let [channel (api/create-channel ctx community-id
                        {:name name :type ch-type :visibility visibility})]
          ;; Navigate to the new channel
          (datastar-redirect-response (str "/app/channel/" (:id channel))))
        (catch clojure.lang.ExceptionInfo e
          (let [data (ex-data e)
                msg  (or (get-in data [:body :error :message])
                         (ex-message e)
                         "Failed to create channel")]
            (datastar-fragment-response
              [:div {:id "create-channel-error" :class "text-red text-sm"} msg])))))))

;;; ---------------------------------------------------------------------------
;;; Health Check
;;; ---------------------------------------------------------------------------

(defn health-handler
  "GET /web/health -- health check returning NATS connection status."
  [request]
  (let [nats   (get-in request [:system :nats])
        conns  (get-in request [:system :connections])
        healthy (and nats (eventbus/connected? nats))]
    {:status  (if healthy 200 503)
     :headers {"Content-Type" "application/json"}
     :body    (str "{\"status\":\"" (if healthy "ok" "degraded") "\","
                   "\"nats\":" (if healthy "true" "false") ","
                   "\"active_connections\":" (count @conns) "}")}))
