(ns ajet.chat.api.handlers.invites
  "Invite management handlers.

   Invites are links with unique codes that allow users to join a community.
   They can have optional max_uses and expiry. Direct invites create a
   notification for the target user."
  (: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]
           [java.security SecureRandom]))

;;; ---------------------------------------------------------------------------
;;; 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- generate-invite-code
  "Generate a random alphanumeric invite code."
  []
  (let [chars "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        sr    (SecureRandom.)
        sb    (StringBuilder. 8)]
    (dotimes [_ 8]
      (.append sb (.charAt chars (.nextInt sr (count chars)))))
    (.toString sb)))

(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 create-invite
  "POST /api/communities/:cid/invites
   Creates an invite link. Admin+ only."
  [request]
  (let [user-id      (require-user! request)
        ds           (get-in request [:system :ds])
        community-id (get-in request [:path-params :cid])
        params       (:body-params request)
        max-uses     (:max_uses params)
        expires-hrs  (:expires_in_hours params)]

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

    (let [invite-id (uuid)
          code      (generate-invite-code)
          values    (cond-> {:id           [:cast invite-id :uuid]
                             :community-id [:cast community-id :uuid]
                             :created-by   [:cast user-id :uuid]
                             :code         code}
                      max-uses    (assoc :max-uses max-uses)
                      expires-hrs (assoc :expires-at
                                    [:raw (str "now() + interval '" expires-hrs " hours'")]))]

      (db/execute! ds
        {:insert-into :invites
         :values [values]})

      (let [invite (db/execute-one! ds
                     {:select [:*]
                      :from   [:invites]
                      :where  [:= :id [:cast invite-id :uuid]]})]
        (mw/json-response 201 (assoc invite :url (str "/invite/" code)))))))

(defn list-invites
  "GET /api/communities/:cid/invites
   Lists active (non-expired, non-exhausted) invites. Admin+ only."
  [request]
  (let [user-id      (require-user! request)
        ds           (get-in request [:system :ds])
        community-id (get-in request [:path-params :cid])]

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

    (let [invites (db/execute! ds
                    {:select [:*]
                     :from   [:invites]
                     :where  [:and
                              [:= :community-id [:cast community-id :uuid]]
                              [:or
                               [:= :expires-at nil]
                               [:> :expires-at [:now]]]
                              [:or
                               [:= :max-uses nil]
                               [:< :uses :max-uses]]]
                     :order-by [[:created-at :desc]]})]
      (mw/json-response invites))))

(defn revoke-invite
  "DELETE /api/invites/:id
   Deletes an invite. Admin+ of the invite's community."
  [request]
  (let [user-id   (require-user! request)
        ds        (get-in request [:system :ds])
        invite-id (get-in request [:path-params :id])
        invite    (or (db/execute-one! ds
                        {:select [:*]
                         :from   [:invites]
                         :where  [:= :id [:cast invite-id :uuid]]})
                      (throw (ex-info "Invite not found"
                                      {:type :ajet.chat/not-found})))]

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

    (db/execute! ds
      {:delete-from :invites
       :where       [:= :id [:cast invite-id :uuid]]})

    (mw/json-response 204 nil)))

(defn accept-invite
  "POST /api/invites/:code/accept
   Accepts an invite and joins the community. Idempotent if already a member."
  [request]
  (let [user-id (require-user! request)
        ds      (get-in request [:system :ds])
        nats    (get-in request [:system :nats])
        code    (get-in request [:path-params :code])
        invite  (or (db/execute-one! ds
                      {:select [:*]
                       :from   [:invites]
                       :where  [:= :code code]})
                    (throw (ex-info "Invite not found"
                                    {:type :ajet.chat/not-found})))
        community-id (str (:community-id invite))]

    ;; Check expiry
    (when (:expires-at invite)
      (let [expires-at (:expires-at invite)
            now        (java.time.Instant/now)
            exp-inst   (if (instance? java.time.Instant expires-at)
                         expires-at
                         (.toInstant expires-at))]
        (when (.isAfter now exp-inst)
          (throw (ex-info "This invite has expired"
                          {:type :ajet.chat/gone})))))

    ;; Check max uses
    (when (and (:max-uses invite) (>= (:uses invite) (:max-uses invite)))
      (throw (ex-info "This invite has reached its maximum number of uses"
                      {:type :ajet.chat/gone})))

    ;; Check if user is banned
    (when (db/execute-one! ds
            {:select [[:1 :exists]]
             :from   [:bans]
             :where  [:and
                      [:= :community-id [:cast community-id :uuid]]
                      [:= :user-id [:cast user-id :uuid]]]})
      (throw (ex-info "You are banned from this community"
                      {:type :ajet.chat/forbidden})))

    ;; Check if already a member (idempotent)
    (let [already-member (db/execute-one! ds
                           {:select [:user-id]
                            :from   [:community-members]
                            :where  [:and
                                     [:= :community-id [:cast community-id :uuid]]
                                     [:= :user-id [:cast user-id :uuid]]]})]
      (if already-member
        (mw/json-response {:status "already_member" :community-id community-id})

        (do
          (db/with-transaction [tx ds]
            ;; Add as member
            (db/execute! tx
              {:insert-into :community-members
               :values [{:community-id [:cast community-id :uuid]
                         :user-id      [:cast user-id :uuid]
                         :role         "member"}]})

            ;; Join #general channel
            (when-let [general (db/execute-one! tx
                                 {:select [:id]
                                  :from   [:channels]
                                  :where  [:and
                                           [:= :community-id [:cast community-id :uuid]]
                                           [:= :name "general"]]})]
              (db/execute! tx
                {:insert-into   :channel-members
                 :values        [{:channel-id (:id general)
                                  :user-id    [:cast user-id :uuid]}]
                 :on-conflict   [:channel-id :user-id]
                 :do-nothing    true}))

            ;; Increment invite uses
            (db/execute! tx
              {:update :invites
               :set    {:uses [:+ :uses 1]}
               :where  [:= :id (:id invite)]}))

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

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

(defn direct-invite
  "POST /api/communities/:cid/invites/direct
   Creates a direct invite for a specific user by ID. Admin+ only.
   Creates an invite + a notification for the target user."
  [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)
        target-uid   (:user_id params)]

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

    (when (str/blank? target-uid)
      (throw (ex-info "user_id is required"
                      {:type :ajet.chat/validation-error})))

    ;; Verify target user exists
    (when-not (db/execute-one! ds
                {:select [:id]
                 :from   [:users]
                 :where  [:= :id [:cast target-uid :uuid]]})
      (throw (ex-info "Target user not found"
                      {:type :ajet.chat/not-found})))

    (let [invite-id (uuid)
          code      (generate-invite-code)
          notif-id  (uuid)]

      (db/with-transaction [tx ds]
        ;; Create invite (single use)
        (db/execute! tx
          {:insert-into :invites
           :values [{:id           [:cast invite-id :uuid]
                     :community-id [:cast community-id :uuid]
                     :created-by   [:cast user-id :uuid]
                     :code         code
                     :max-uses     1}]})

        ;; Create notification for target user
        (db/execute! tx
          {:insert-into :notifications
           :values [{:id        [:cast notif-id :uuid]
                     :user-id   [:cast target-uid :uuid]
                     :type      "invite"
                     :source-id [:cast invite-id :uuid]}]}))

      ;; Publish notification event
      (publish-event! nats
        (str "chat.notifications." target-uid)
        :notification/new
        {:user-id      target-uid
         :type         "invite"
         :invite-code  code
         :community-id community-id
         :invited-by   user-id})

      (let [invite (db/execute-one! ds
                     {:select [:*]
                      :from   [:invites]
                      :where  [:= :id [:cast invite-id :uuid]]})]
        (mw/json-response 201 (assoc invite :url (str "/invite/" code)))))))
