(ns ajet.chat.api.handlers.commands
  "Slash command dispatcher.

   Parses command strings and dispatches to the appropriate handler.
   Commands: /help, /topic, /nick, /invite, /kick, /ban, /mute,
   /token, /webhook, /status"
  (: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- parse-user-mention
  "Extract user UUID from @<user:uuid> syntax."
  [s]
  (when-let [[_ uid] (re-matches #"@<user:([0-9a-f-]+)>" s)]
    uid))

(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- generate-token []
  (let [bytes (byte-array 32)
        sr    (SecureRandom.)]
    (.nextBytes sr bytes)
    (.encodeToString (Base64/getUrlEncoder) bytes)))

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

(defn- generate-invite-code []
  (let [chars "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        sr    (SecureRandom.)
        sb    (StringBuilder. 8)]
    (dotimes [_ 8]
      (.append sb (.charAt chars (.nextInt sr (count chars)))))
    (.toString sb)))

;;; ---------------------------------------------------------------------------
;;; Command Handlers
;;; ---------------------------------------------------------------------------

(def ^:private help-text
  {"help"    {:description "Show available commands"    :usage "/help [command]"           :permission "All"}
   "topic"   {:description "Set channel topic"          :usage "/topic <text>"             :permission "Admin+"}
   "nick"    {:description "Set community nickname"     :usage "/nick <nickname>"          :permission "All"}
   "invite"  {:description "Generate invite link"       :usage "/invite [max_uses] [expires_hours]" :permission "Admin+"}
   "kick"    {:description "Kick user from community"   :usage "/kick @<user>"             :permission "Admin+"}
   "ban"     {:description "Ban user from community"    :usage "/ban @<user>"              :permission "Admin+"}
   "mute"    {:description "Mute user for duration"     :usage "/mute @<user> <duration>"  :permission "Admin+"}
   "token"   {:description "Manage API tokens"          :usage "/token create|revoke|list" :permission "Owner"}
   "webhook" {:description "Manage webhooks"            :usage "/webhook create|delete|list" :permission "Admin+"}
   "status"  {:description "Set status text"            :usage "/status <text>"            :permission "All"}})

(defn- cmd-help [_ds _nats _user-id _community-id _channel-id args]
  (if (and (seq args) (get help-text (first args)))
    (let [cmd (get help-text (first args))]
      {:result (str "**/" (first args) "** - " (:description cmd)
                 "\nUsage: `" (:usage cmd) "`"
                 "\nPermission: " (:permission cmd))})
    {:result (str "**Available commands:**\n"
               (str/join "\n"
                 (map (fn [[name {:keys [description]}]]
                        (str "  `/" name "` - " description))
                   (sort-by key help-text))))}))

(defn- cmd-topic [ds nats user-id community-id channel-id args]
  (check-role! ds community-id user-id "admin")
  (when (empty? args)
    (throw (ex-info "Usage: /topic <text>" {:type :ajet.chat/validation-error})))
  (let [topic (str/join " " args)]
    (db/execute! ds
      {:update :channels
       :set    {:topic topic}
       :where  [:= :id [:cast channel-id :uuid]]})
    (publish-event! nats
      (str "chat.events." community-id)
      :channel/updated
      {:channel-id channel-id :topic topic :updated-by user-id})
    {:result (str "Channel topic set to: " topic)}))

(defn- cmd-nick [ds _nats user-id community-id _channel-id args]
  (when (empty? args)
    (throw (ex-info "Usage: /nick <nickname>" {:type :ajet.chat/validation-error})))
  (let [nickname (str/join " " args)]
    (db/execute! ds
      {:update :community-members
       :set    {:nickname nickname}
       :where  [:and
                [:= :community-id [:cast community-id :uuid]]
                [:= :user-id [:cast user-id :uuid]]]})
    {:result (str "Nickname set to: " nickname)}))

(defn- cmd-invite [ds _nats user-id community-id _channel-id args]
  (check-role! ds community-id user-id "admin")
  (let [max-uses    (some-> (first args) parse-long)
        expires-hrs (some-> (second args) parse-long)
        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]})
    {:result (str "Invite created: `/invite/" code "`"
               (when max-uses (str " (max " max-uses " uses)"))
               (when expires-hrs (str " (expires in " expires-hrs "h)")))}))

(defn- cmd-kick [ds nats user-id community-id _channel-id args]
  (check-role! ds community-id user-id "admin")
  (when (empty? args)
    (throw (ex-info "Usage: /kick @<user>" {:type :ajet.chat/validation-error})))
  (let [target-uid (or (parse-user-mention (first args))
                       (throw (ex-info "Invalid user mention. Use @<user:uuid> format."
                                       {:type :ajet.chat/validation-error})))
        target (or (db/execute-one! ds
                     {:select [:role]
                      :from   [:community-members]
                      :where  [:and
                               [:= :community-id [:cast community-id :uuid]]
                               [:= :user-id [:cast target-uid :uuid]]]})
                   (throw (ex-info "User is not a member of this community"
                                   {:type :ajet.chat/not-found})))]
    (when (= (:role target) "owner")
      (throw (ex-info "Cannot kick the community owner"
                      {:type :ajet.chat/forbidden})))
    ;; Remove from channels
    (db/execute-sql! ds
      ["DELETE FROM channel_members WHERE user_id = ?::uuid AND channel_id IN (SELECT id FROM channels WHERE community_id = ?::uuid)"
       target-uid community-id])
    ;; Remove community membership
    (db/execute! ds
      {:delete-from :community-members
       :where       [:and
                     [:= :community-id [:cast community-id :uuid]]
                     [:= :user-id [:cast target-uid :uuid]]]})
    (publish-event! nats
      (str "chat.events." community-id)
      :member/kicked
      {:community-id community-id :user-id target-uid :kicked-by user-id})
    {:result (str "User " target-uid " has been kicked from the community.")}))

(defn- cmd-ban [ds nats user-id community-id _channel-id args]
  (check-role! ds community-id user-id "admin")
  (when (empty? args)
    (throw (ex-info "Usage: /ban @<user>" {:type :ajet.chat/validation-error})))
  (let [target-uid (or (parse-user-mention (first args))
                       (throw (ex-info "Invalid user mention. Use @<user:uuid> format."
                                       {:type :ajet.chat/validation-error})))
        target (db/execute-one! ds
                 {:select [:role]
                  :from   [:community-members]
                  :where  [:and
                           [:= :community-id [:cast community-id :uuid]]
                           [:= :user-id [:cast target-uid :uuid]]]})]
    (when (and target (= (:role target) "owner"))
      (throw (ex-info "Cannot ban the community owner"
                      {:type :ajet.chat/forbidden})))
    (let [reason (when (> (count args) 1) (str/join " " (rest args)))]
      ;; Create ban record
      (db/execute! ds
        {:insert-into   :bans
         :values        [(cond-> {:community-id [:cast community-id :uuid]
                                  :user-id      [:cast target-uid :uuid]
                                  :banned-by    [:cast user-id :uuid]}
                           reason (assoc :reason reason))]
         :on-conflict   [:community-id :user-id]
         :do-nothing    true})
      ;; Remove from channels
      (db/execute-sql! ds
        ["DELETE FROM channel_members WHERE user_id = ?::uuid AND channel_id IN (SELECT id FROM channels WHERE community_id = ?::uuid)"
         target-uid community-id])
      ;; Remove community membership
      (db/execute! ds
        {:delete-from :community-members
         :where       [:and
                       [:= :community-id [:cast community-id :uuid]]
                       [:= :user-id [:cast target-uid :uuid]]]})
      (publish-event! nats
        (str "chat.events." community-id)
        :member/banned
        {:community-id community-id :user-id target-uid :banned-by user-id :reason reason})
      {:result (str "User " target-uid " has been banned from the community.")})))

(defn- parse-duration
  "Parse duration string like '10m', '1h', '24h', '7d' to SQL interval."
  [s]
  (when-let [[_ n unit] (re-matches #"(\d+)(m|h|d)" s)]
    (let [unit-name (case unit "m" "minutes" "h" "hours" "d" "days")]
      (str n " " unit-name))))

(defn- cmd-mute [ds nats user-id community-id _channel-id args]
  (check-role! ds community-id user-id "admin")
  (when (< (count args) 2)
    (throw (ex-info "Usage: /mute @<user> <duration> (e.g. 10m, 1h, 24h, 7d)"
                    {:type :ajet.chat/validation-error})))
  (let [target-uid (or (parse-user-mention (first args))
                       (throw (ex-info "Invalid user mention. Use @<user:uuid> format."
                                       {:type :ajet.chat/validation-error})))
        duration-str (second args)
        interval     (or (parse-duration duration-str)
                         (throw (ex-info "Invalid duration. Use format like: 10m, 1h, 24h, 7d"
                                         {:type :ajet.chat/validation-error})))]
    ;; Check target is not owner
    (let [target (db/execute-one! ds
                   {:select [:role]
                    :from   [:community-members]
                    :where  [:and
                             [:= :community-id [:cast community-id :uuid]]
                             [:= :user-id [:cast target-uid :uuid]]]})]
      (when (and target (= (:role target) "owner"))
        (throw (ex-info "Cannot mute the community owner"
                        {:type :ajet.chat/forbidden}))))

    ;; Upsert mute record
    (db/execute-sql! ds
      [(str "INSERT INTO mutes (community_id, user_id, expires_at, muted_by) "
            "VALUES (?::uuid, ?::uuid, now() + interval '" interval "', ?::uuid) "
            "ON CONFLICT (community_id, user_id) "
            "DO UPDATE SET expires_at = now() + interval '" interval "', muted_by = ?::uuid")
       community-id target-uid user-id user-id])

    {:result (str "User " target-uid " has been muted for " duration-str ".")}))

(defn- cmd-token [ds _nats user-id community-id _channel-id args]
  (check-role! ds community-id user-id "owner")
  (let [sub-cmd (first args)]
    (case sub-cmd
      "create"
      (let [name-v (or (second args)
                       (throw (ex-info "Usage: /token create <name>"
                                       {:type :ajet.chat/validation-error})))
            api-user-id (uuid)
            token-id    (uuid)
            token       (generate-token)
            token-h     (hash-token token)]
        (db/with-transaction [tx ds]
          (db/execute! tx
            {:insert-into :api-users
             :values [{:id           [:cast api-user-id :uuid]
                       :name         name-v
                       :community-id [:cast community-id :uuid]
                       :created-by   [:cast user-id :uuid]}]})
          (db/execute! tx
            {:insert-into :api-tokens
             :values [{:id          [:cast token-id :uuid]
                       :api-user-id [:cast api-user-id :uuid]
                       :token-hash  token-h}]}))
        {:result (str "API token created for **" name-v "**\n"
                   "Token: `" token "`\n"
                   "This token will only be shown once. Save it securely.")})

      "revoke"
      (let [name-v (or (second args)
                       (throw (ex-info "Usage: /token revoke <name>"
                                       {:type :ajet.chat/validation-error})))
            api-user (db/execute-one! ds
                       {:select [:id]
                        :from   [:api-users]
                        :where  [:and
                                 [:= :name name-v]
                                 [:= :community-id [:cast community-id :uuid]]]})]
        (when-not api-user
          (throw (ex-info (str "API user not found: " name-v)
                          {:type :ajet.chat/not-found})))
        ;; Delete the api_user (CASCADE deletes tokens)
        (db/execute! ds
          {:delete-from :api-users
           :where       [:= :id (:id api-user)]})
        {:result (str "API token for **" name-v "** has been revoked.")})

      "list"
      (let [api-users (db/execute! ds
                        {:select [:au.id :au.name :au.created-at]
                         :from   [[:api-users :au]]
                         :where  [:= :au.community-id [:cast community-id :uuid]]
                         :order-by [[:au.created-at :desc]]})]
        (if (empty? api-users)
          {:result "No API tokens configured."}
          {:result (str "**API Tokens:**\n"
                     (str/join "\n" (map #(str "  - " (:name %) " (created " (:created-at %) ")")
                                     api-users)))}))

      ;; default
      (throw (ex-info "Usage: /token create|revoke|list <name>"
                      {:type :ajet.chat/validation-error})))))

(defn- cmd-webhook [ds _nats user-id community-id channel-id args]
  (check-role! ds community-id user-id "admin")
  (let [sub-cmd (first args)]
    (case sub-cmd
      "create"
      (let [name-v     (or (second args)
                           (throw (ex-info "Usage: /webhook create <name> [channel-id]"
                                           {:type :ajet.chat/validation-error})))
            target-ch  (or (nth args 2 nil) channel-id)
            webhook-id (uuid)
            token      (generate-token)
            token-h    (hash-token token)]
        (db/execute! ds
          {:insert-into :webhooks
           :values [{:id           [:cast webhook-id :uuid]
                     :community-id [:cast community-id :uuid]
                     :channel-id   [:cast target-ch :uuid]
                     :name         name-v
                     :token-hash   token-h
                     :created-by   [:cast user-id :uuid]}]})
        {:result (str "Webhook **" name-v "** created.\n"
                   "URL: `/api/webhooks/" webhook-id "/incoming`\n"
                   "Token: `" token "`\n"
                   "This token will only be shown once.")})

      "delete"
      (let [name-v (or (second args)
                       (throw (ex-info "Usage: /webhook delete <name>"
                                       {:type :ajet.chat/validation-error})))
            webhook (db/execute-one! ds
                      {:select [:id]
                       :from   [:webhooks]
                       :where  [:and
                                [:= :name name-v]
                                [:= :community-id [:cast community-id :uuid]]]})]
        (when-not webhook
          (throw (ex-info (str "Webhook not found: " name-v)
                          {:type :ajet.chat/not-found})))
        (db/execute! ds
          {:delete-from :webhooks
           :where       [:= :id (:id webhook)]})
        {:result (str "Webhook **" name-v "** deleted.")})

      "list"
      (let [webhooks (db/execute! ds
                       {:select [:name :channel-id :created-at]
                        :from   [:webhooks]
                        :where  [:= :community-id [:cast community-id :uuid]]
                        :order-by [[:created-at :desc]]})]
        (if (empty? webhooks)
          {:result "No webhooks configured."}
          {:result (str "**Webhooks:**\n"
                     (str/join "\n" (map #(str "  - " (:name %)
                                            " -> channel " (:channel-id %))
                                     webhooks)))}))

      ;; default
      (throw (ex-info "Usage: /webhook create|delete|list <name>"
                      {:type :ajet.chat/validation-error})))))

(defn- cmd-status [ds _nats user-id _community-id _channel-id args]
  (let [status-text (str/join " " args)]
    (db/execute! ds
      {:update :users
       :set    {:status-text (when (seq status-text) status-text)}
       :where  [:= :id [:cast user-id :uuid]]})
    {:result (if (seq status-text)
               (str "Status set to: " status-text)
               "Status cleared.")}))

;;; ---------------------------------------------------------------------------
;;; Dispatcher
;;; ---------------------------------------------------------------------------

(def ^:private command-handlers
  {"help"    cmd-help
   "topic"   cmd-topic
   "nick"    cmd-nick
   "invite"  cmd-invite
   "kick"    cmd-kick
   "ban"     cmd-ban
   "mute"    cmd-mute
   "token"   cmd-token
   "webhook" cmd-webhook
   "status"  cmd-status})

(defn execute-command
  "POST /api/commands
   Parses and dispatches a slash command.
   Body: {\"command\": \"/kick @<user:uuid>\", \"channel_id\": \"uuid\", \"community_id\": \"uuid\"}"
  [request]
  (let [user-id      (require-user! request)
        ds           (get-in request [:system :ds])
        nats         (get-in request [:system :nats])
        params       (:body-params request)
        command-str  (:command params)
        channel-id   (:channel_id params)
        community-id (:community_id params)]

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

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

    ;; Parse command: "/cmd arg1 arg2 ..."
    (let [parts    (str/split (str/trim command-str) #"\s+")
          cmd-name (when (str/starts-with? (first parts) "/")
                     (subs (first parts) 1))
          args     (rest parts)]

      (when-not cmd-name
        (throw (ex-info "Command must start with /"
                        {:type :ajet.chat/validation-error})))

      (if-let [handler-fn (get command-handlers cmd-name)]
        (let [result (handler-fn ds nats user-id community-id channel-id args)]
          (mw/json-response result))

        ;; Unknown command
        (mw/json-response {:result (str "Unknown command: /" cmd-name ". Type /help for available commands.")})))))
