(ns ajet.chat.api.handlers.reactions
  "Reaction handlers: add, remove, and list reactions on messages.

   Reactions use a composite PK (message_id, user_id, emoji) so each
   user can only have one reaction per emoji per message."
  (:require [clojure.tools.logging :as log]
            [ajet.chat.shared.db :as db]
            [ajet.chat.shared.eventbus :as eventbus]
            [ajet.chat.api.middleware :as mw]))

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

(defn- require-user! [request]
  (or (:user-id request)
      (throw (ex-info "Authentication required" {:type :ajet.chat/forbidden}))))

(defn- check-channel-member! [ds channel-id user-id]
  (or (db/execute-one! ds
        {:select [:user-id]
         :from   [:channel-members]
         :where  [:and
                  [:= :channel-id [:cast channel-id :uuid]]
                  [:= :user-id [:cast user-id :uuid]]]})
      (throw (ex-info "Not a member of this channel"
                      {:type :ajet.chat/forbidden}))))

(defn- get-message-row [ds message-id]
  (or (db/execute-one! ds
        {:select [:*]
         :from   [:messages]
         :where  [:= :id [:cast message-id :uuid]]})
      (throw (ex-info "Message not found" {:type :ajet.chat/not-found}))))

(defn- get-channel-row [ds channel-id]
  (db/execute-one! ds
    {:select [:*]
     :from   [:channels]
     :where  [:= :id [:cast channel-id :uuid]]}))

(defn- publish-event! [nats subject event-type payload]
  (try
    (eventbus/publish! nats subject event-type payload)
    (catch Exception e
      (log/error e "Failed to publish event" event-type "to" subject))))

;;; ---------------------------------------------------------------------------
;;; Handlers
;;; ---------------------------------------------------------------------------

(defn add-reaction
  "PUT /api/messages/:id/reactions/:emoji
   Adds a reaction. Idempotent (no error if already exists)."
  [request]
  (let [user-id    (require-user! request)
        ds         (get-in request [:system :ds])
        nats       (get-in request [:system :nats])
        message-id (get-in request [:path-params :id])
        emoji      (get-in request [:path-params :emoji])
        message    (get-message-row ds message-id)
        channel-id (str (:channel-id message))]

    (check-channel-member! ds channel-id user-id)

    ;; Upsert: insert if not exists (ON CONFLICT DO NOTHING)
    (db/execute! ds
      {:insert-into   :reactions
       :values        [{:message-id [:cast message-id :uuid]
                        :user-id    [:cast user-id :uuid]
                        :emoji      emoji}]
       :on-conflict   [:message-id :user-id :emoji]
       :do-nothing    true})

    (let [channel (get-channel-row ds channel-id)
          subject (if (:community-id channel)
                    (str "chat.events." (:community-id channel))
                    (str "chat.dm." channel-id))]
      (publish-event! nats subject :reaction/added
        {:message-id message-id
         :channel-id channel-id
         :user-id    user-id
         :emoji      emoji}))

    (mw/json-response {:status "ok" :emoji emoji})))

(defn remove-reaction
  "DELETE /api/messages/:id/reactions/:emoji
   Removes a reaction by the current user."
  [request]
  (let [user-id    (require-user! request)
        ds         (get-in request [:system :ds])
        nats       (get-in request [:system :nats])
        message-id (get-in request [:path-params :id])
        emoji      (get-in request [:path-params :emoji])
        message    (get-message-row ds message-id)
        channel-id (str (:channel-id message))]

    (let [deleted (db/execute! ds
                    {:delete-from :reactions
                     :where       [:and
                                   [:= :message-id [:cast message-id :uuid]]
                                   [:= :user-id [:cast user-id :uuid]]
                                   [:= :emoji emoji]]})]
      ;; Check if anything was deleted
      (when (zero? (:next.jdbc/update-count (first deleted) 0))
        (throw (ex-info "Reaction not found"
                        {:type :ajet.chat/not-found}))))

    (let [channel (get-channel-row ds channel-id)
          subject (if (:community-id channel)
                    (str "chat.events." (:community-id channel))
                    (str "chat.dm." channel-id))]
      (publish-event! nats subject :reaction/removed
        {:message-id message-id
         :channel-id channel-id
         :user-id    user-id
         :emoji      emoji}))

    (mw/json-response 204 nil)))

(defn list-reactions
  "GET /api/messages/:id/reactions
   Returns reactions grouped by emoji with user lists."
  [request]
  (let [user-id    (require-user! request)
        ds         (get-in request [:system :ds])
        message-id (get-in request [:path-params :id])
        message    (get-message-row ds message-id)]

    (check-channel-member! ds (str (:channel-id message)) user-id)

    (let [reactions (db/execute! ds
                      {:select [:r.emoji :r.user-id :u.username :u.display-name]
                       :from   [[:reactions :r]]
                       :join   [[:users :u] [:= :u.id :r.user-id]]
                       :where  [:= :r.message-id [:cast message-id :uuid]]
                       :order-by [[:r.emoji :asc] [:r.created-at :asc]]})
          ;; Group by emoji
          grouped  (reduce (fn [acc reaction]
                             (update acc (:emoji reaction)
                               (fnil conj [])
                               {:user-id      (str (:user-id reaction))
                                :username     (:username reaction)
                                :display-name (:display-name reaction)}))
                     {} reactions)
          result   (mapv (fn [[emoji users]]
                           {:emoji emoji
                            :count (count users)
                            :users users})
                     grouped)]
      (mw/json-response result))))
