(ns ajet.chat.api.handlers.channels
  "Channel CRUD, join/leave, and member listing handlers.

   Channels belong to a community. DM channels have nil community_id
   and are handled separately in the DMs handler."
  (:require [clojure.string :as str]
            [clojure.tools.logging :as log]
            [ajet.chat.shared.db :as db]
            [ajet.chat.shared.eventbus :as eventbus]
            [ajet.chat.api.middleware :as mw])
  (:import [java.util UUID]))

;;; ---------------------------------------------------------------------------
;;; 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-membership! [ds community-id user-id]
  (or (db/execute-one! ds
        {:select [:*]
         :from   [:community-members]
         :where  [:and
                  [:= :community-id [:cast community-id :uuid]]
                  [:= :user-id [:cast user-id :uuid]]]})
      (throw (ex-info "Not a member of this community"
                      {:type :ajet.chat/forbidden}))))

(defn- check-role! [ds community-id user-id required-role]
  (let [member (check-membership! ds community-id user-id)
        role   (:role member)
        hierarchy {"owner" 3 "admin" 2 "member" 1}
        has-level (get hierarchy role 0)
        need-level (get hierarchy required-role 0)]
    (when (< has-level need-level)
      (throw (ex-info (str "Requires " required-role " role or higher")
                      {:type :ajet.chat/forbidden})))
    member))

(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- 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 list-channels
  "GET /api/communities/:cid/channels
   Returns public channels + private channels user is a member of."
  [request]
  (let [user-id      (require-user! request)
        ds           (get-in request [:system :ds])
        community-id (get-in request [:path-params :cid])
        _            (check-membership! ds community-id user-id)
        channels     (db/execute! ds
                       {:select-distinct [:c.*]
                        :from   [[:channels :c]]
                        :left-join [[:channel-members :cm]
                                    [:and
                                     [:= :cm.channel-id :c.id]
                                     [:= :cm.user-id [:cast user-id :uuid]]]]
                        :where  [:and
                                 [:= :c.community-id [:cast community-id :uuid]]
                                 [:or
                                  [:= :c.visibility "public"]
                                  [:!= :cm.user-id nil]]]
                        :order-by [[:c.name :asc]]})]
    (mw/json-response channels)))

(defn create-channel
  "POST /api/communities/:cid/channels
   Creates a channel. Admin+ only. Adds creator as member."
  [request]
  (let [user-id      (require-user! request)
        ds           (get-in request [:system :ds])
        nats         (get-in request [:system :nats])
        community-id (get-in request [:path-params :cid])
        params       (:body-params request)
        name-v       (:name params)
        ch-type      (or (:type params) "text")
        visibility   (or (:visibility params) "public")
        topic        (:topic params)
        category-id  (:category_id params)]

    (check-role! ds community-id user-id "admin")

    (when (str/blank? name-v)
      (throw (ex-info "Channel name is required"
                      {:type :ajet.chat/validation-error})))

    ;; Check for duplicate name in community
    (when (db/execute-one! ds
            {:select [:id]
             :from   [:channels]
             :where  [:and
                      [:= :community-id [:cast community-id :uuid]]
                      [:= :name name-v]]})
      (throw (ex-info "A channel with this name already exists in this community"
                      {:type :ajet.chat/conflict})))

    (let [channel-id (uuid)]
      (db/with-transaction [tx ds]
        (db/execute! tx
          {:insert-into :channels
           :values [(cond-> {:id           [:cast channel-id :uuid]
                             :community-id [:cast community-id :uuid]
                             :name         name-v
                             :type         ch-type
                             :visibility   visibility}
                      topic       (assoc :topic topic)
                      category-id (assoc :category-id [:cast category-id :uuid]))]})

        ;; Add creator as channel member
        (db/execute! tx
          {:insert-into :channel-members
           :values [{:channel-id [:cast channel-id :uuid]
                     :user-id    [:cast user-id :uuid]}]}))

      (let [channel (get-channel-row ds channel-id)]
        (publish-event! nats
          (str "chat.events." community-id)
          :channel/created
          {:channel-id   channel-id
           :community-id community-id
           :name         name-v
           :type         ch-type
           :visibility   visibility
           :created-by   user-id})

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

(defn get-channel
  "GET /api/channels/:id
   Returns channel details. Accessible to channel members, or community members for public channels."
  [request]
  (let [user-id    (require-user! request)
        ds         (get-in request [:system :ds])
        channel-id (get-in request [:path-params :id])
        channel    (get-channel-row ds channel-id)]

    ;; Access check: channel member OR (public channel + community member)
    (let [is-channel-member (db/execute-one! ds
                              {:select [:user-id]
                               :from   [:channel-members]
                               :where  [:and
                                        [:= :channel-id [:cast channel-id :uuid]]
                                        [:= :user-id [:cast user-id :uuid]]]})]
      (when-not is-channel-member
        (if (and (= "public" (:visibility channel))
                 (:community-id channel))
          (check-membership! ds (str (:community-id channel)) user-id)
          (throw (ex-info "Not a member of this channel"
                          {:type :ajet.chat/forbidden})))))

    (mw/json-response channel)))

(defn update-channel
  "PUT /api/channels/:id
   Updates channel name, topic, category. Admin+ of the community."
  [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)
        channel    (get-channel-row ds channel-id)
        community-id (str (:community-id channel))]

    (check-role! ds community-id user-id "admin")

    (let [updates (cond-> {}
                    (:name params)        (assoc :name (:name params))
                    (:topic params)       (assoc :topic (:topic params))
                    (contains? params :category_id)
                    (assoc :category-id (when (:category_id params)
                                          [:cast (:category_id params) :uuid]))
                    (:visibility params)  (assoc :visibility (:visibility params)))]
      (when (seq updates)
        (db/execute! ds
          {:update :channels
           :set    updates
           :where  [:= :id [:cast channel-id :uuid]]}))

      (let [updated (get-channel-row ds channel-id)]
        (publish-event! nats
          (str "chat.events." community-id)
          :channel/updated
          {:channel-id   channel-id
           :community-id community-id
           :updated-by   user-id
           :changes      (dissoc updates :category-id)})

        (mw/json-response updated)))))

(defn delete-channel
  "DELETE /api/channels/:id
   Deletes channel and all messages. Admin+ of the community."
  [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])
        channel    (get-channel-row ds channel-id)
        community-id (str (:community-id channel))]

    (check-role! ds community-id user-id "admin")

    ;; CASCADE handles cleanup of messages, members, etc.
    (db/execute! ds
      {:delete-from :channels
       :where       [:= :id [:cast channel-id :uuid]]})

    (publish-event! nats
      (str "chat.events." community-id)
      :channel/deleted
      {:channel-id   channel-id
       :community-id community-id
       :name         (:name channel)
       :deleted-by   user-id})

    (mw/json-response 204 nil)))

(defn join-channel
  "POST /api/channels/:id/join
   Joins a public channel. Community membership required."
  [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])
        channel    (get-channel-row ds channel-id)
        community-id (str (:community-id channel))]

    (check-membership! ds community-id user-id)

    (when (= "private" (:visibility channel))
      (throw (ex-info "Cannot join a private channel without an invite"
                      {:type :ajet.chat/forbidden})))

    ;; Idempotent: ignore if already a member
    (let [existing (db/execute-one! ds
                     {:select [:user-id]
                      :from   [:channel-members]
                      :where  [:and
                               [:= :channel-id [:cast channel-id :uuid]]
                               [:= :user-id [:cast user-id :uuid]]]})]
      (when-not existing
        (db/execute! ds
          {:insert-into :channel-members
           :values [{:channel-id [:cast channel-id :uuid]
                     :user-id    [:cast user-id :uuid]}]})

        (publish-event! nats
          (str "chat.events." community-id)
          :member/joined
          {:channel-id   channel-id
           :community-id community-id
           :user-id      user-id})))

    (mw/json-response {:status "joined" :channel-id channel-id})))

(defn leave-channel
  "POST /api/channels/:id/leave
   Leaves a channel."
  [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])
        channel    (get-channel-row ds channel-id)
        community-id (when (:community-id channel)
                       (str (:community-id channel)))]

    (db/execute! ds
      {:delete-from :channel-members
       :where       [:and
                     [:= :channel-id [:cast channel-id :uuid]]
                     [:= :user-id [:cast user-id :uuid]]]})

    (when community-id
      (publish-event! nats
        (str "chat.events." community-id)
        :member/left
        {:channel-id   channel-id
         :community-id community-id
         :user-id      user-id}))

    (mw/json-response {:status "left" :channel-id channel-id})))

(defn list-channel-members
  "GET /api/channels/:id/members
   Lists members of a channel. Requires channel membership."
  [request]
  (let [user-id    (require-user! request)
        ds         (get-in request [:system :ds])
        channel-id (get-in request [:path-params :id])]

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

    (let [members (db/execute! ds
                    {:select [:u.id :u.username :u.display-name :u.avatar-url
                              :u.status-text :cm.joined-at]
                     :from   [[:channel-members :cm]]
                     :join   [[:users :u] [:= :u.id :cm.user-id]]
                     :where  [:= :cm.channel-id [:cast channel-id :uuid]]
                     :order-by [[:u.username :asc]]})]
      (mw/json-response members))))
