(ns ajet.chat.auth-gw.middleware
  "Ring middleware for the Auth Gateway.

   Pipeline order (outermost first):
   1. wrap-exception-handler — catch-all error handler
   2. wrap-cors             — CORS headers and OPTIONS preflight
   3. wrap-trace-id         — ensure X-Trace-Id on every request
   4. wrap-rate-limit       — token-bucket rate limiting"
  (:require [clojure.string :as str]
            [clojure.tools.logging :as log]
            [ajet.chat.auth-gw.rate-limit :as rl]
            [ajet.chat.auth-gw.pages :as pages])
  (:import [java.io ByteArrayInputStream InputStream]))

;;; ---------------------------------------------------------------------------
;;; Body buffering (for proxy + wrap-params coexistence)
;;; ---------------------------------------------------------------------------

(defn wrap-buffer-body
  "Buffer the request body so it can be read by both wrap-params and the proxy.
   Without this, wrap-params consumes the InputStream and the proxy gets an empty body."
  [handler]
  (fn [request]
    (let [body (:body request)]
      (if (instance? InputStream body)
        (let [bytes (.readAllBytes ^InputStream body)
              request (assoc request
                             :body (ByteArrayInputStream. bytes)
                             :raw-body bytes)]
          (handler request))
        (handler request)))))

;;; ---------------------------------------------------------------------------
;;; Exception handler
;;; ---------------------------------------------------------------------------

(defn wrap-exception-handler
  "Catch-all middleware that turns unhandled exceptions into 500 responses."
  [handler]
  (fn [request]
    (try
      (handler request)
      (catch Exception e
        (let [trace-id (get-in request [:headers "x-trace-id"] "unknown")]
          (log/error e "Unhandled exception" {:trace-id trace-id
                                               :uri      (:uri request)
                                               :method   (:request-method request)})
          {:status  500
           :headers {"Content-Type" "text/html; charset=utf-8"
                     "X-Trace-Id"   trace-id}
           :body    (pages/error-page {:status  500
                                       :title   "Internal Server Error"
                                       :message "An unexpected error occurred. Please try again later."})})))))

;;; ---------------------------------------------------------------------------
;;; CORS
;;; ---------------------------------------------------------------------------

(defn- origin-allowed?
  "Check if the request Origin is in the allowed list.
   In dev mode, allow any localhost origin."
  [origin allowed-origins dev-mode?]
  (cond
    (str/blank? origin) false
    dev-mode?           (or (str/starts-with? origin "http://localhost")
                            (str/starts-with? origin "http://127.0.0.1")
                            (contains? (set allowed-origins) origin))
    :else               (contains? (set allowed-origins) origin)))

(defn- cors-headers
  "Build CORS response headers."
  [origin config]
  (let [methods (or (:allowed-methods config)
                    [:get :post :put :delete :options])
        headers-list (or (:allowed-headers config)
                         ["Content-Type" "Authorization" "X-Trace-Id"])
        max-age (or (:max-age config) 86400)]
    {"Access-Control-Allow-Origin"      origin
     "Access-Control-Allow-Methods"     (str/join ", " (map #(str/upper-case (name %)) methods))
     "Access-Control-Allow-Headers"     (str/join ", " headers-list)
     "Access-Control-Allow-Credentials" "true"
     "Access-Control-Max-Age"           (str max-age)}))

(defn wrap-cors
  "CORS middleware — adds CORS headers and handles OPTIONS preflight requests."
  [handler {:keys [cors] :as config}]
  (let [allowed-origins (:allowed-origins cors)
        dev-mode?       (not (get-in config [:session :cookie-secure] true))]
    (fn [request]
      (let [origin (get-in request [:headers "origin"])]
        (if (and (= :options (:request-method request)) origin)
          ;; Preflight request — respond immediately
          (if (origin-allowed? origin allowed-origins dev-mode?)
            {:status  204
             :headers (cors-headers origin cors)
             :body    ""}
            {:status  403
             :headers {"Content-Type" "text/plain"}
             :body    "CORS origin not allowed"})
          ;; Normal request — add CORS headers to response
          (let [response (handler request)]
            (if (and origin (origin-allowed? origin allowed-origins dev-mode?))
              (update response :headers merge (cors-headers origin cors))
              response)))))))

;;; ---------------------------------------------------------------------------
;;; Trace ID
;;; ---------------------------------------------------------------------------

(defn wrap-trace-id
  "Ensure every request has an X-Trace-Id header. Generates one if missing.
   Also adds the trace ID to the response."
  [handler]
  (fn [request]
    (let [existing (get-in request [:headers "x-trace-id"])
          trace-id (or existing (str (java.util.UUID/randomUUID)))
          request  (if existing
                     request
                     (assoc-in request [:headers "x-trace-id"] trace-id))
          response (handler request)]
      (assoc-in response [:headers "X-Trace-Id"] trace-id))))

;;; ---------------------------------------------------------------------------
;;; Rate limiting
;;; ---------------------------------------------------------------------------

(defn wrap-rate-limit
  "Apply rate limiting based on route classification.

   The limiter-atom is created at startup and shared across all requests.
   Rate limit categories are determined by the request URI and method."
  [handler limiter-atom rate-limit-config]
  (if-not (:enabled rate-limit-config)
    handler ;; Rate limiting disabled
    (fn [request]
      (let [classification (rl/classify-request request)]
        (if-not classification
          ;; No rate limit applies
          (handler request)
          (let [[category identity-key] classification
                result (rl/check-rate-limit! limiter-atom category identity-key)]
            (if (:allowed? result)
              (handler request)
              ;; Rate limited
              (let [retry-after-s (max 1 (long (Math/ceil (/ (:retry-after-ms result 1000) 1000.0))))]
                (log/warn "Rate limited" category identity-key
                          "retry-after" retry-after-s "s")
                {:status  429
                 :headers {"Content-Type" "text/html; charset=utf-8"
                           "Retry-After"  (str retry-after-s)}
                 :body    (pages/error-page
                            {:status      429
                             :title       "Too Many Requests"
                             :message     "You're making requests too quickly. Please slow down."
                             :retry-after retry-after-s})}))))))))

;;; ---------------------------------------------------------------------------
;;; Request logging
;;; ---------------------------------------------------------------------------

(defn wrap-request-logging
  "Log each request with method, path, status, and duration."
  [handler]
  (fn [request]
    (let [method   (str/upper-case (name (:request-method request)))
          path     (:uri request)
          trace-id (get-in request [:headers "x-trace-id"] "?")
          start    (System/nanoTime)
          response (handler request)
          duration (/ (- (System/nanoTime) start) 1e6)]
      (log/info (format "[%s] %s %s %d (%.0fms)"
                        trace-id method path
                        (:status response 500) duration))
      response)))
