(ns ajet.chat.shared.storage
  "S3-compatible file storage client for MinIO.

   Operations: upload!, download, delete!, presigned-url.
   Files stored with key format: attachments/{uuid}/{filename}."
  (:require [clojure.tools.logging :as log])
  (:import [software.amazon.awssdk.services.s3 S3Client S3Configuration S3Configuration$Builder]
           [software.amazon.awssdk.services.s3.model
            PutObjectRequest GetObjectRequest DeleteObjectRequest
            HeadBucketRequest CreateBucketRequest
            NoSuchBucketException]
           [software.amazon.awssdk.services.s3.presigner S3Presigner]
           [software.amazon.awssdk.services.s3.presigner.model GetObjectPresignRequest]
           [software.amazon.awssdk.core.sync RequestBody]
           [software.amazon.awssdk.auth.credentials StaticCredentialsProvider AwsBasicCredentials]
           [software.amazon.awssdk.regions Region]
           [java.net URI]
           [java.time Duration]
           [java.io InputStream]))

(def ^:private max-file-size (* 10 1024 1024)) ;; 10MB

(def ^:private allowed-content-types
  #{"image/jpeg" "image/png" "image/gif" "image/webp"})

;;; ---------------------------------------------------------------------------
;;; Client Creation
;;; ---------------------------------------------------------------------------

(defn make-client
  "Create an S3 client for MinIO.

   Config shape: {:endpoint \"http://localhost:9000\"
                  :access-key \"minioadmin\"
                  :secret-key \"minioadmin\"
                  :bucket \"ajet-chat\"}"
  [{:keys [endpoint access-key secret-key]}]
  (let [creds (StaticCredentialsProvider/create
                (AwsBasicCredentials/create access-key secret-key))]
    (-> (S3Client/builder)
        (.endpointOverride (URI. endpoint))
        (.credentialsProvider creds)
        (.region Region/US_EAST_1)
        (.serviceConfiguration
          (reify java.util.function.Consumer
            (accept [_ builder]
              (.pathStyleAccessEnabled ^S3Configuration$Builder builder true))))
        (.build))))

(defn make-presigner
  "Create an S3 presigner for generating temporary download URLs."
  [{:keys [endpoint access-key secret-key]}]
  (let [creds (StaticCredentialsProvider/create
                (AwsBasicCredentials/create access-key secret-key))]
    (-> (S3Presigner/builder)
        (.endpointOverride (URI. endpoint))
        (.credentialsProvider creds)
        (.region Region/US_EAST_1)
        (.build))))

(defn close-client
  "Close an S3 client."
  [^S3Client client]
  (.close client))

;;; ---------------------------------------------------------------------------
;;; Bucket Management
;;; ---------------------------------------------------------------------------

(defn ensure-bucket!
  "Create bucket if it doesn't exist."
  [^S3Client client bucket]
  (try
    (.headBucket client (-> (HeadBucketRequest/builder)
                            (.bucket bucket)
                            (.build)))
    (log/info "Bucket exists:" bucket)
    (catch NoSuchBucketException _
      (.createBucket client (-> (CreateBucketRequest/builder)
                                (.bucket bucket)
                                (.build)))
      (log/info "Created bucket:" bucket))))

;;; ---------------------------------------------------------------------------
;;; Validation
;;; ---------------------------------------------------------------------------

(defn validate-upload!
  "Validate content-type and file size. Throws ex-info on invalid."
  [content-type size-bytes]
  (when-not (allowed-content-types content-type)
    (throw (ex-info "Invalid content type. Only images allowed (JPEG, PNG, GIF, WebP)."
                    {:type         :ajet.chat/validation-error
                     :content-type content-type
                     :allowed      allowed-content-types})))
  (when (> size-bytes max-file-size)
    (throw (ex-info (str "File too large. Max size: 10MB, got: " (/ size-bytes 1024 1024.0) "MB")
                    {:type       :ajet.chat/validation-error
                     :size-bytes size-bytes
                     :max-bytes  max-file-size}))))

(defn storage-key
  "Generate storage key: attachments/{uuid}/{filename}."
  [attachment-id filename]
  (str "attachments/" attachment-id "/" filename))

;;; ---------------------------------------------------------------------------
;;; Operations
;;; ---------------------------------------------------------------------------

(defn upload!
  "Upload bytes to storage.

   Returns the storage key."
  [^S3Client client bucket key ^bytes data content-type]
  (let [req (-> (PutObjectRequest/builder)
                (.bucket bucket)
                (.key key)
                (.contentType content-type)
                (.contentLength (long (alength data)))
                (.build))]
    (.putObject client req (RequestBody/fromBytes data))
    (log/info "Uploaded:" key "(" (alength data) "bytes)")
    key))

(defn upload-stream!
  "Upload an InputStream to storage."
  [^S3Client client bucket key ^InputStream stream content-type content-length]
  (let [req (-> (PutObjectRequest/builder)
                (.bucket bucket)
                (.key key)
                (.contentType content-type)
                (.contentLength (long content-length))
                (.build))]
    (.putObject client req (RequestBody/fromInputStream stream content-length))
    (log/info "Uploaded:" key "(" content-length "bytes)")
    key))

(defn download
  "Download file bytes from storage. Returns byte array or nil if not found."
  [^S3Client client bucket key]
  (try
    (let [req (-> (GetObjectRequest/builder)
                  (.bucket bucket)
                  (.key key)
                  (.build))]
      (.readAllBytes (.getObject client req)))
    (catch software.amazon.awssdk.services.s3.model.NoSuchKeyException _
      nil)))

(defn delete!
  "Delete a file from storage."
  [^S3Client client bucket key]
  (let [req (-> (DeleteObjectRequest/builder)
                (.bucket bucket)
                (.key key)
                (.build))]
    (.deleteObject client req)
    (log/info "Deleted:" key)))

(defn presigned-url
  "Generate a temporary pre-signed download URL (default: 1 hour)."
  [^S3Presigner presigner bucket key & [{:keys [duration-minutes] :or {duration-minutes 60}}]]
  (let [get-req (-> (GetObjectRequest/builder)
                    (.bucket bucket)
                    (.key key)
                    (.build))
        presign-req (-> (GetObjectPresignRequest/builder)
                        (.signatureDuration (Duration/ofMinutes duration-minutes))
                        (.getObjectRequest get-req)
                        (.build))]
    (str (.url (.presignGetObject presigner presign-req)))))
