(ns ajet.chat.shared.api-client
  "HTTP client SDK for the ajet-chat API.

   All public functions take an explicit context map (ctx) as the first argument.

   ctx shape:
     {:base-url   \"http://localhost:3001\"
      :auth-token \"base64url-token\"
      :trace-id   \"uuid\"
      :user-id    \"uuid\"
      :user-role  \"admin\"
      :timeout    10000}

   Error handling:
     - HTTP 4xx/5xx -> ex-info with {:type :ajet.chat/api-error, :status, :body, :trace-id}
     - Network errors -> propagated from http-client"
  (:require [babashka.http-client :as http]
            [clojure.data.json :as json]
            [clojure.java.io :as io]
            [clojure.string :as str]
            [clojure.tools.logging :as log]))

;;; ---------------------------------------------------------------------------
;;; Private HTTP layer
;;; ---------------------------------------------------------------------------

(defn- api-url [base-url path]
  (let [base (cond-> base-url (str/ends-with? base-url "/") (subs 0 (dec (count base-url))))
        p    (cond-> path (str/starts-with? path "/") (subs 1))]
    (str base "/" p)))

(defn- build-headers [ctx]
  (cond-> {"Accept" "application/json"}
    (:auth-token ctx) (assoc "Authorization" (str "Bearer " (:auth-token ctx)))
    (:trace-id ctx)   (assoc "X-Trace-Id" (:trace-id ctx))
    (:user-id ctx)    (assoc "X-User-Id" (:user-id ctx))
    (:user-role ctx)  (assoc "X-User-Role" (:user-role ctx))))

(defn- encode-json [data]
  (json/write-str data))

(defn- parse-json [s]
  (when-not (str/blank? s)
    (json/read-str s :key-fn keyword)))

(defn- check-response! [response trace-id]
  (let [status (:status response)]
    (when (>= status 400)
      (throw (ex-info (str "API error: HTTP " status)
                      {:type     :ajet.chat/api-error
                       :status   status
                       :body     (parse-json (:body response))
                       :trace-id trace-id})))))

(defn- request!
  "Core HTTP dispatch with optional retry on 5xx."
  [ctx method path & [{:keys [body query-params multipart]}]]
  (let [headers    (cond-> (build-headers ctx)
                     (and body (not multipart)) (assoc "Content-Type" "application/json"))
        url        (api-url (:base-url ctx) path)
        trace-id   (:trace-id ctx)
        timeout    (or (:timeout ctx) 10000)
        base-opts  (cond-> {:method  method
                            :uri     url
                            :headers headers
                            :throw   false
                            :timeout timeout}
                     (and body (not multipart)) (assoc :body (encode-json body))
                     multipart                  (assoc :multipart multipart)
                     query-params               (assoc :query-params query-params))
        max-retries 3]
    (loop [attempt 1]
      (let [response (http/request base-opts)
            status   (:status response)]
        (if (and (>= status 500) (< attempt max-retries))
          (do
            (log/warn "Retrying request" method url "attempt" (inc attempt) "status" status)
            (Thread/sleep (* attempt 500))
            (recur (inc attempt)))
          (do
            (check-response! response trace-id)
            (parse-json (:body response))))))))

;;; ---------------------------------------------------------------------------
;;; Context helper
;;; ---------------------------------------------------------------------------

(defn request->ctx
  "Build an API client ctx from a Ring request and the API base URL."
  [ring-request api-base-url]
  (let [headers (:headers ring-request)
        auth    (get headers "authorization")
        token   (when auth
                  (let [parts (str/split auth #"\s+" 2)]
                    (when (= "Bearer" (first parts))
                      (second parts))))]
    (cond-> {:base-url api-base-url}
      token                           (assoc :auth-token token)
      (get headers "x-trace-id")      (assoc :trace-id (get headers "x-trace-id"))
      (get headers "x-user-id")       (assoc :user-id (get headers "x-user-id"))
      (get headers "x-user-role")     (assoc :user-role (get headers "x-user-role"))
      (get headers "x-community-id")  (assoc :community-id (get headers "x-community-id")))))

;;; ---------------------------------------------------------------------------
;;; Communities
;;; ---------------------------------------------------------------------------

(defn create-community [ctx body-map]
  (request! ctx :post "/api/communities" {:body body-map}))

(defn get-communities [ctx]
  (request! ctx :get "/api/communities"))

(defn get-community [ctx community-id]
  (request! ctx :get (str "/api/communities/" community-id)))

(defn update-community [ctx community-id body-map]
  (request! ctx :put (str "/api/communities/" community-id) {:body body-map}))

(defn delete-community [ctx community-id]
  (request! ctx :delete (str "/api/communities/" community-id)))

;;; ---------------------------------------------------------------------------
;;; Channels
;;; ---------------------------------------------------------------------------

(defn get-channels [ctx community-id]
  (request! ctx :get (str "/api/communities/" community-id "/channels")))

(defn create-channel [ctx community-id body-map]
  (request! ctx :post (str "/api/communities/" community-id "/channels") {:body body-map}))

(defn get-channel [ctx channel-id]
  (request! ctx :get (str "/api/channels/" channel-id)))

(defn update-channel [ctx channel-id body-map]
  (request! ctx :put (str "/api/channels/" channel-id) {:body body-map}))

(defn delete-channel [ctx channel-id]
  (request! ctx :delete (str "/api/channels/" channel-id)))

(defn join-channel [ctx channel-id]
  (request! ctx :post (str "/api/channels/" channel-id "/join")))

(defn leave-channel [ctx channel-id]
  (request! ctx :post (str "/api/channels/" channel-id "/leave")))

(defn get-channel-members [ctx channel-id]
  (request! ctx :get (str "/api/channels/" channel-id "/members")))

;;; ---------------------------------------------------------------------------
;;; Channel Categories
;;; ---------------------------------------------------------------------------

(defn get-categories [ctx community-id]
  (request! ctx :get (str "/api/communities/" community-id "/categories")))

(defn create-category [ctx community-id body-map]
  (request! ctx :post (str "/api/communities/" community-id "/categories") {:body body-map}))

(defn update-category [ctx category-id body-map]
  (request! ctx :put (str "/api/categories/" category-id) {:body body-map}))

(defn delete-category [ctx category-id]
  (request! ctx :delete (str "/api/categories/" category-id)))

;;; ---------------------------------------------------------------------------
;;; Messages
;;; ---------------------------------------------------------------------------

(defn get-messages
  "Fetch messages for a channel. opts: :before :after :limit"
  [ctx channel-id & [opts]]
  (let [qp (cond-> {}
              (:before opts) (assoc "before" (str (:before opts)))
              (:after opts)  (assoc "after" (str (:after opts)))
              (:limit opts)  (assoc "limit" (str (:limit opts))))]
    (request! ctx :get (str "/api/channels/" channel-id "/messages")
              (when (seq qp) {:query-params qp}))))

(defn get-message [ctx message-id]
  (request! ctx :get (str "/api/messages/" message-id)))

(defn send-message [ctx channel-id body-map]
  (request! ctx :post (str "/api/channels/" channel-id "/messages") {:body body-map}))

(defn edit-message [ctx message-id body-map]
  (request! ctx :put (str "/api/messages/" message-id) {:body body-map}))

(defn delete-message [ctx message-id]
  (request! ctx :delete (str "/api/messages/" message-id)))

(defn get-thread [ctx message-id]
  (request! ctx :get (str "/api/messages/" message-id "/thread")))

;;; ---------------------------------------------------------------------------
;;; Reactions
;;; ---------------------------------------------------------------------------

(defn add-reaction [ctx message-id emoji]
  (request! ctx :put (str "/api/messages/" message-id "/reactions/" emoji)))

(defn remove-reaction [ctx message-id emoji]
  (request! ctx :delete (str "/api/messages/" message-id "/reactions/" emoji)))

(defn get-reactions [ctx message-id]
  (request! ctx :get (str "/api/messages/" message-id "/reactions")))

;;; ---------------------------------------------------------------------------
;;; DMs
;;; ---------------------------------------------------------------------------

(defn get-dms [ctx]
  (request! ctx :get "/api/dms"))

(defn create-dm [ctx body-map]
  (request! ctx :post "/api/dms" {:body body-map}))

(defn create-group-dm [ctx body-map]
  (request! ctx :post "/api/dms/group" {:body body-map}))

;;; ---------------------------------------------------------------------------
;;; Users & Profiles
;;; ---------------------------------------------------------------------------

(defn get-me [ctx]
  (request! ctx :get "/api/me"))

(defn update-me [ctx body-map]
  (request! ctx :put "/api/me" {:body body-map}))

(defn get-user [ctx user-id]
  (request! ctx :get (str "/api/users/" user-id)))

(defn get-community-members [ctx community-id]
  (request! ctx :get (str "/api/communities/" community-id "/members")))

(defn update-community-member [ctx community-id user-id body-map]
  (request! ctx :put (str "/api/communities/" community-id "/members/" user-id) {:body body-map}))

(defn kick-member [ctx community-id user-id]
  (request! ctx :delete (str "/api/communities/" community-id "/members/" user-id)))

;;; ---------------------------------------------------------------------------
;;; Notifications
;;; ---------------------------------------------------------------------------

(defn get-notifications [ctx & [opts]]
  (let [qp (cond-> {}
              (some? (:unread opts)) (assoc "unread" (str (:unread opts)))
              (:after opts)          (assoc "after" (str (:after opts)))
              (:limit opts)          (assoc "limit" (str (:limit opts))))]
    (request! ctx :get "/api/notifications"
              (when (seq qp) {:query-params qp}))))

(defn mark-notifications-read [ctx body-map]
  (request! ctx :post "/api/notifications/read" {:body body-map}))

(defn get-unread-count [ctx]
  (request! ctx :get "/api/notifications/unread-count"))

;;; ---------------------------------------------------------------------------
;;; Presence & Typing
;;; ---------------------------------------------------------------------------

(defn heartbeat [ctx]
  (request! ctx :post "/api/heartbeat"))

(defn get-presence [ctx community-id]
  (request! ctx :get (str "/api/communities/" community-id "/presence")))

;;; ---------------------------------------------------------------------------
;;; Search
;;; ---------------------------------------------------------------------------

(defn search [ctx opts]
  (let [qp (cond-> {"q" (:q opts)}
              (:type opts)         (assoc "type" (name (:type opts)))
              (:community-id opts) (assoc "community_id" (str (:community-id opts)))
              (:channel-id opts)   (assoc "channel_id" (str (:channel-id opts)))
              (:from opts)         (assoc "from" (str (:from opts)))
              (:after opts)        (assoc "after" (str (:after opts)))
              (:before opts)       (assoc "before" (str (:before opts)))
              (:limit opts)        (assoc "limit" (str (:limit opts))))]
    (request! ctx :get "/api/search" {:query-params qp})))

;;; ---------------------------------------------------------------------------
;;; Invites
;;; ---------------------------------------------------------------------------

(defn create-invite [ctx community-id body-map]
  (request! ctx :post (str "/api/communities/" community-id "/invites") {:body body-map}))

(defn get-invites [ctx community-id]
  (request! ctx :get (str "/api/communities/" community-id "/invites")))

(defn revoke-invite [ctx invite-id]
  (request! ctx :delete (str "/api/invites/" invite-id)))

(defn accept-invite [ctx code]
  (request! ctx :post (str "/api/invites/" code "/accept")))

(defn direct-invite [ctx community-id body-map]
  (request! ctx :post (str "/api/communities/" community-id "/invites/direct") {:body body-map}))

;;; ---------------------------------------------------------------------------
;;; Webhooks
;;; ---------------------------------------------------------------------------

(defn create-webhook [ctx community-id body-map]
  (request! ctx :post (str "/api/communities/" community-id "/webhooks") {:body body-map}))

(defn get-webhooks [ctx community-id]
  (request! ctx :get (str "/api/communities/" community-id "/webhooks")))

(defn delete-webhook [ctx webhook-id]
  (request! ctx :delete (str "/api/webhooks/" webhook-id)))

;;; ---------------------------------------------------------------------------
;;; Slash Commands
;;; ---------------------------------------------------------------------------

(defn execute-command [ctx body-map]
  (request! ctx :post "/api/commands" {:body body-map}))

;;; ---------------------------------------------------------------------------
;;; File Upload
;;; ---------------------------------------------------------------------------

(defn upload-file [ctx channel-id file-path content-type]
  (request! ctx :post (str "/api/channels/" channel-id "/upload")
            {:multipart [{:name "file" :content (io/file file-path)
                          :content-type content-type}]}))

;;; ---------------------------------------------------------------------------
;;; Read Tracking
;;; ---------------------------------------------------------------------------

(defn mark-read [ctx channel-id body-map]
  (request! ctx :post (str "/api/channels/" channel-id "/read") {:body body-map}))

;;; ---------------------------------------------------------------------------
;;; Health
;;; ---------------------------------------------------------------------------

(defn health [ctx]
  (request! ctx :get "/api/health"))
