(ns ajet.chat.api.handlers.messages
  "Message handlers: send, list, get, edit, delete, thread, and read-tracking.

   Messages use cursor-based pagination. The send handler parses mentions,
   creates mention records, creates notifications for @mentioned users,
   and publishes events to the correct NATS subject."
  (:require [clojure.string :as str]
            [clojure.tools.logging :as log]
            [ajet.chat.shared.db :as db]
            [ajet.chat.shared.eventbus :as eventbus]
            [ajet.chat.shared.mentions :as mentions]
            [ajet.chat.api.middleware :as mw])
  (:import [java.util UUID]
           [java.time Instant Duration]))

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

(defn- uuid [] (str (UUID/randomUUID)))

(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 [:*]
         :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-channel-row [ds channel-id]
  (or (db/execute-one! ds
        {:select [:*]
         :from   [:channels]
         :where  [:= :id [:cast channel-id :uuid]]})
      (throw (ex-info "Channel not found" {:type :ajet.chat/not-found}))))

(defn- get-message-row [ds message-id]
  (or (db/execute-one! ds
        {:select [:m.* :u.username :u.display-name :u.avatar-url]
         :from   [[:messages :m]]
         :join   [[:users :u] [:= :m.user-id :u.id]]
         :where  [:= :m.id [:cast message-id :uuid]]})
      (throw (ex-info "Message not found" {:type :ajet.chat/not-found}))))

(defn- nats-subject-for-channel
  "Determine the NATS subject based on channel type.
   Community channels -> chat.events.{community-id}
   DM channels -> chat.dm.{channel-id}"
  [channel]
  (if (:community-id channel)
    (str "chat.events." (:community-id channel))
    (str "chat.dm." (:id channel))))

(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))))

(defn- create-mention-records!
  "Parse mentions from message body, create mention rows, and create notifications."
  [ds message-id body-md channel-id user-id]
  (let [parsed (mentions/parse body-md)]
    (doseq [{:keys [type id]} parsed]
      (let [mention-id (uuid)]
        (db/execute! ds
          {:insert-into :mentions
           :values [(cond-> {:id          [:cast mention-id :uuid]
                             :message-id  [:cast message-id :uuid]
                             :target-type (name type)}
                      id (assoc :target-id [:cast id :uuid]))]}))

      ;; Create notifications for user mentions
      (when (and (= type :user) id)
        (let [notif-id (uuid)]
          (db/execute! ds
            {:insert-into :notifications
             :values [{:id        [:cast notif-id :uuid]
                       :user-id   [:cast id :uuid]
                       :type      "mention"
                       :source-id [:cast message-id :uuid]}]})))

      ;; For @here mentions, notify all channel members (except sender)
      (when (= type :here)
        (let [members (db/execute! ds
                        {:select [:user-id]
                         :from   [:channel-members]
                         :where  [:and
                                  [:= :channel-id [:cast channel-id :uuid]]
                                  [:!= :user-id [:cast user-id :uuid]]]})]
          (doseq [member members]
            (let [notif-id (uuid)]
              (db/execute! ds
                {:insert-into :notifications
                 :values [{:id        [:cast notif-id :uuid]
                           :user-id   (:user-id member)
                           :type      "mention"
                           :source-id [:cast message-id :uuid]}]}))))))))

(defn- create-thread-notifications!
  "Notify thread participants (except the sender) when a thread reply is created."
  [ds message-id parent-id user-id]
  (let [participants (db/execute! ds
                       {:select-distinct [:user-id]
                        :from   [:messages]
                        :where  [:and
                                 [:or
                                  [:= :id [:cast parent-id :uuid]]
                                  [:= :parent-id [:cast parent-id :uuid]]]
                                 [:!= :user-id [:cast user-id :uuid]]]})]
    (doseq [participant participants]
      (let [notif-id (uuid)]
        (db/execute! ds
          {:insert-into :notifications
           :values [{:id        [:cast notif-id :uuid]
                     :user-id   (:user-id participant)
                     :type      "thread_reply"
                     :source-id [:cast message-id :uuid]}]})))))

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

(defn list-messages
  "GET /api/channels/:id/messages?before=<uuid>&after=<uuid>&limit=N
   Cursor-based pagination. Default limit 50, max 100."
  [request]
  (let [user-id    (require-user! request)
        ds         (get-in request [:system :ds])
        channel-id (get-in request [:path-params :id])
        params     (:query-params request)
        before     (get params "before")
        after      (get params "after")
        limit      (min (or (some-> (get params "limit") parse-long) 50) 100)]

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

    (let [base-where [:= :m.channel-id [:cast channel-id :uuid]]
          ;; Cursor-based pagination using subquery on created_at
          where-clause (cond
                         before
                         [:and base-where
                          [:< :m.created-at
                           {:select [:created-at]
                            :from   [:messages]
                            :where  [:= :id [:cast before :uuid]]}]]

                         after
                         [:and base-where
                          [:> :m.created-at
                           {:select [:created-at]
                            :from   [:messages]
                            :where  [:= :id [:cast after :uuid]]}]]

                         :else base-where)
          order-dir (if after :asc :desc)
          messages  (db/execute! ds
                      {:select   [:m.* :u.username :u.display-name :u.avatar-url]
                       :from     [[:messages :m]]
                       :join     [[:users :u] [:= :m.user-id :u.id]]
                       :where    where-clause
                       :order-by [[:m.created-at order-dir]]
                       :limit    limit})
          ;; Always return newest-last ordering
          messages  (if (= order-dir :desc)
                      (vec (reverse messages))
                      messages)]
      (mw/json-response messages))))

(defn send-message
  "POST /api/channels/:id/messages
   Creates message, parses mentions, creates notifications, publishes event."
  [request]
  (let [user-id    (require-user! request)
        ds         (get-in request [:system :ds])
        nats       (get-in request [:system :nats])
        channel-id (get-in request [:path-params :id])
        params     (:body-params request)
        body-md    (:body_md params)
        parent-id  (:parent_id params)]

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

    (when (str/blank? body-md)
      (throw (ex-info "Message body is required"
                      {:type :ajet.chat/validation-error})))

    ;; If reply, resolve parent to root (no nested threads)
    (let [resolved-parent-id
          (when parent-id
            (let [parent (get-message-row ds parent-id)]
              (or (:parent-id parent) (str (:id parent)))))
          message-id (uuid)]

      (db/with-transaction [tx ds]
        (db/execute! tx
          {:insert-into :messages
           :values [(cond-> {:id         [:cast message-id :uuid]
                             :channel-id [:cast channel-id :uuid]
                             :user-id    [:cast user-id :uuid]
                             :body-md    body-md}
                      resolved-parent-id
                      (assoc :parent-id [:cast resolved-parent-id :uuid]))]})

        ;; Create mention records and notifications
        (create-mention-records! tx message-id body-md channel-id user-id)

        ;; Create thread notifications if this is a reply
        (when resolved-parent-id
          (create-thread-notifications! tx message-id resolved-parent-id user-id)))

      ;; Publish event with user info for real-time display
      (let [channel (get-channel-row ds channel-id)
            subject (nats-subject-for-channel channel)
            message (get-message-row ds message-id)
            user    (db/execute-one! ds
                      {:select [:username :display-name :avatar-url]
                       :from   [:users]
                       :where  [:= :id [:cast user-id :uuid]]})]

        (publish-event! nats subject :message/created
          (merge message
                 {:username     (:username user)
                  :display-name (:display-name user)
                  :avatar-url   (:avatar-url user)
                  :community-id (when (:community-id channel)
                                  (str (:community-id channel)))}))

        (mw/json-response 201 message)))))

(defn get-message
  "GET /api/messages/:id
   Returns a single message. Requires channel membership."
  [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)
    (mw/json-response message)))

(defn edit-message
  "PUT /api/messages/:id
   Edits message body. Author only, within 1-hour window.
   Re-parses mentions and publishes :message/edited."
  [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])
        params     (:body-params request)
        new-body   (:body_md params)
        message    (get-message-row ds message-id)]

    ;; Author check
    (when (not= (str (:user-id message)) user-id)
      (throw (ex-info "Only the author can edit this message"
                      {:type :ajet.chat/forbidden})))

    ;; 1-hour edit window
    (let [created-at (:created-at message)
          created-inst (if (instance? java.time.Instant created-at)
                         created-at
                         (.toInstant created-at))
          one-hour-later (.plus created-inst (Duration/ofMinutes 60))
          now (Instant/now)]
      (when (.isAfter now one-hour-later)
        (throw (ex-info "Edit window has expired (1 hour)"
                        {:type :ajet.chat/forbidden}))))

    (when (str/blank? new-body)
      (throw (ex-info "Message body is required"
                      {:type :ajet.chat/validation-error})))

    (db/with-transaction [tx ds]
      ;; Update message
      (db/execute! tx
        {:update :messages
         :set    {:body-md   new-body
                  :edited-at [:now]}
         :where  [:= :id [:cast message-id :uuid]]})

      ;; Delete old mentions and re-create
      (db/execute! tx
        {:delete-from :mentions
         :where       [:= :message-id [:cast message-id :uuid]]})

      (create-mention-records! tx message-id new-body
        (str (:channel-id message)) user-id))

    (let [updated (get-message-row ds message-id)
          channel (get-channel-row ds (str (:channel-id message)))
          subject (nats-subject-for-channel channel)]

      (publish-event! nats subject :message/edited
        {:message-id   message-id
         :channel-id   (str (:channel-id message))
         :user-id      user-id
         :body-md      new-body
         :community-id (when (:community-id channel)
                         (str (:community-id channel)))})

      (mw/json-response updated))))

(defn delete-message
  "DELETE /api/messages/:id
   Deletes message. Author or admin+ of the community."
  [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])
        message    (get-message-row ds message-id)
        channel    (get-channel-row ds (str (:channel-id message)))
        is-author  (= (str (:user-id message)) user-id)]

    ;; Check permission: author or admin+ in the community
    (when-not is-author
      (if (:community-id channel)
        (let [member (db/execute-one! ds
                       {:select [:role]
                        :from   [:community-members]
                        :where  [:and
                                 [:= :community-id (:community-id channel)]
                                 [:= :user-id [:cast user-id :uuid]]]})
              role   (:role member)
              hierarchy {"owner" 3 "admin" 2 "member" 1}]
          (when (< (get hierarchy role 0) (get hierarchy "admin" 0))
            (throw (ex-info "Only the author or an admin can delete this message"
                            {:type :ajet.chat/forbidden}))))
        ;; For DMs, only the author can delete
        (throw (ex-info "Only the author can delete this message"
                        {:type :ajet.chat/forbidden}))))

    ;; CASCADE handles mentions, reactions, attachments
    (db/execute! ds
      {:delete-from :messages
       :where       [:= :id [:cast message-id :uuid]]})

    (let [subject (nats-subject-for-channel channel)]
      (publish-event! nats subject :message/deleted
        {:message-id   message-id
         :channel-id   (str (:channel-id message))
         :user-id      user-id
         :community-id (when (:community-id channel)
                         (str (:community-id channel)))}))

    (mw/json-response 204 nil)))

(defn get-thread
  "GET /api/messages/:id/thread
   Returns all replies to a message, ordered by created_at."
  [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)

    ;; Get root message + all replies (with user info)
    (let [replies (db/execute! ds
                    {:select   [:m.* :u.username :u.display-name :u.avatar-url]
                     :from     [[:messages :m]]
                     :join     [[:users :u] [:= :m.user-id :u.id]]
                     :where    [:= :m.parent-id [:cast message-id :uuid]]
                     :order-by [[:m.created-at :asc]]})]
      (mw/json-response {:root    message
                          :replies replies}))))

(defn mark-read
  "POST /api/channels/:id/read
   Updates last_read_message_id for the user in this channel."
  [request]
  (let [user-id    (require-user! request)
        ds         (get-in request [:system :ds])
        channel-id (get-in request [:path-params :id])
        params     (:body-params request)
        msg-id     (:last_read_message_id params)]

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

    (when (str/blank? msg-id)
      (throw (ex-info "last_read_message_id is required"
                      {:type :ajet.chat/validation-error})))

    (db/execute! ds
      {:update :channel-members
       :set    {:last-read-message-id [:cast msg-id :uuid]}
       :where  [:and
                [:= :channel-id [:cast channel-id :uuid]]
                [:= :user-id [:cast user-id :uuid]]]})

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