(ns ajet.chat.api.handlers.webhooks
  "Webhook handlers: create, list, delete, and incoming message posting.

   Webhooks allow external services to post messages to channels.
   Each webhook has a secret token used for authentication."
  (: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 MessageDigest]
           [java.util Base64]))

;;; ---------------------------------------------------------------------------
;;; 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-role! [ds community-id user-id required-role]
  (let [member (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})))
        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-token
  "Generate a random token string."
  []
  (let [bytes (byte-array 32)
        sr    (SecureRandom.)]
    (.nextBytes sr bytes)
    (.encodeToString (Base64/getUrlEncoder) bytes)))

(defn- hash-token
  "SHA-256 hash a token for storage."
  [token]
  (let [md    (MessageDigest/getInstance "SHA-256")
        bytes (.digest md (.getBytes token "UTF-8"))]
    (.encodeToString (Base64/getUrlEncoder) bytes)))

(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-webhook
  "POST /api/communities/:cid/webhooks
   Creates a webhook. Admin+ only. Returns the token once."
  [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)
        name-v       (:name params)
        channel-id   (:channel_id params)
        avatar-url   (:avatar_url params)]

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

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

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

    ;; Verify channel exists and belongs to community
    (let [channel (db/execute-one! ds
                    {:select [:id :community-id]
                     :from   [:channels]
                     :where  [:and
                              [:= :id [:cast channel-id :uuid]]
                              [:= :community-id [:cast community-id :uuid]]]})]
      (when-not channel
        (throw (ex-info "Channel not found in this community"
                        {:type :ajet.chat/not-found}))))

    (let [webhook-id (uuid)
          token      (generate-token)
          token-h    (hash-token token)]

      (db/execute! ds
        {:insert-into :webhooks
         :values [(cond-> {:id           [:cast webhook-id :uuid]
                           :community-id [:cast community-id :uuid]
                           :channel-id   [:cast channel-id :uuid]
                           :name         name-v
                           :token-hash   token-h
                           :created-by   [:cast user-id :uuid]}
                    avatar-url (assoc :avatar-url avatar-url))]})

      (let [webhook (db/execute-one! ds
                      {:select [:id :community-id :channel-id :name :avatar-url :created-at]
                       :from   [:webhooks]
                       :where  [:= :id [:cast webhook-id :uuid]]})]
        ;; Return the token once (not stored in plaintext)
        (mw/json-response 201 (assoc webhook :token token))))))

(defn list-webhooks
  "GET /api/communities/:cid/webhooks
   Lists webhooks (without tokens). 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 [webhooks (db/execute! ds
                     {:select [:id :community-id :channel-id :name :avatar-url
                               :created-by :created-at]
                      :from   [:webhooks]
                      :where  [:= :community-id [:cast community-id :uuid]]
                      :order-by [[:created-at :desc]]})]
      (mw/json-response webhooks))))

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

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

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

    (mw/json-response 204 nil)))

(defn incoming-webhook
  "POST /api/webhooks/:id/incoming
   Post a message via webhook. Authenticates via Bearer token."
  [request]
  (let [ds         (get-in request [:system :ds])
        nats       (get-in request [:system :nats])
        webhook-id (get-in request [:path-params :id])
        params     (:body-params request)
        content    (:content params)
        username   (:username params)
        avatar-url (:avatar_url params)]

    ;; Authenticate via Bearer token
    (let [auth-header (get-in request [:headers "authorization"])
          token       (when (and auth-header (str/starts-with? auth-header "Bearer "))
                        (subs auth-header 7))
          _ (when-not token
              (throw (ex-info "Authorization required"
                              {:type :ajet.chat/forbidden})))
          token-h     (hash-token token)
          webhook     (or (db/execute-one! ds
                            {:select [:*]
                             :from   [:webhooks]
                             :where  [:and
                                      [:= :id [:cast webhook-id :uuid]]
                                      [:= :token-hash token-h]]})
                          (throw (ex-info "Invalid webhook token"
                                          {:type :ajet.chat/forbidden})))]

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

      ;; Create message in the webhook's channel
      (let [message-id (uuid)
            channel-id (str (:channel-id webhook))
            community-id (str (:community-id webhook))
            body-md    (str content)]

        (db/execute! ds
          {:insert-into :messages
           :values [{:id         [:cast message-id :uuid]
                     :channel-id [:cast channel-id :uuid]
                     ;; webhook messages have nil user_id (webhook is the author)
                     :body-md    body-md}]})

        (publish-event! nats
          (str "chat.events." community-id)
          :message/created
          {:message-id   message-id
           :channel-id   channel-id
           :community-id community-id
           :body-md      body-md
           :webhook-id   webhook-id
           :webhook-name (or username (:name webhook))
           :avatar-url   (or avatar-url (:avatar-url webhook))})

        (mw/json-response 204 nil)))))
