(ns ajet.chat.shared.eventbus
  "NATS pub/sub event bus with JetStream support.

   Events are EDN maps serialized as JSON on the wire.
   Subject hierarchy:
     chat.events.{community-id}
     chat.dm.{channel-id}
     chat.typing.{community-id}.{channel-id}
     chat.presence.{community-id}
     chat.notifications.{user-id}
     chat.audit"
  (:require [clojure.data.json :as json]
            [clojure.tools.logging :as log])
  (:import [io.nats.client Nats Options$Builder Connection Connection$Status
            Dispatcher MessageHandler]
           [io.nats.client.api StreamConfiguration StorageType DiscardPolicy
            ConsumerConfiguration DeliverPolicy StreamInfo]
           [io.nats.client JetStream JetStreamSubscription JetStreamOptions
            PushSubscribeOptions]
           [java.time Duration Instant]
           [java.nio.charset StandardCharsets]))

;;; ---------------------------------------------------------------------------
;;; Connection
;;; ---------------------------------------------------------------------------

(defn connect!
  "Create a NATS connection from config.

   Config shape: {:url \"nats://localhost:4222\"}"
  [{:keys [url] :or {url "nats://localhost:4222"}}]
  (let [opts (-> (Options$Builder.)
                 (.server url)
                 (.connectionTimeout (Duration/ofSeconds 5))
                 (.reconnectWait (Duration/ofSeconds 2))
                 (.maxReconnects -1)
                 (.connectionListener
                   (reify io.nats.client.ConnectionListener
                     (connectionEvent [_ conn event]
                       (log/info "NATS connection event:" (str event)))))
                 (.build))]
    (Nats/connect opts)))

(defn close!
  "Close a NATS connection."
  [^Connection conn]
  (.close conn))

(defn connected?
  "Check if NATS connection is active."
  [^Connection conn]
  (= (.getStatus conn) Connection$Status/CONNECTED))

;;; ---------------------------------------------------------------------------
;;; Event Envelope
;;; ---------------------------------------------------------------------------

(defn- keyword->str
  "Convert a keyword to a string, preserving namespace."
  [kw]
  (if-let [ns (namespace kw)]
    (str ns "/" (name kw))
    (name kw)))

(defn- make-event
  "Wrap a payload in the standard event envelope."
  [event-type payload]
  {:type    (keyword->str event-type)
   :id      (str (java.util.UUID/randomUUID))
   :ts      (.toString (Instant/now))
   :payload payload})

(defn- encode-event
  "Serialize an event map to JSON bytes."
  [event]
  (.getBytes (json/write-str event) StandardCharsets/UTF_8))

(defn- decode-event
  "Deserialize JSON bytes to an event map."
  [^bytes data]
  (json/read-str (String. data StandardCharsets/UTF_8) :key-fn keyword))

;;; ---------------------------------------------------------------------------
;;; Core Pub/Sub
;;; ---------------------------------------------------------------------------

(defn publish!
  "Publish an event to a NATS subject.

   event-type: keyword like :message/created
   subject: string like \"chat.events.community-uuid\"
   payload: map of event data"
  [^Connection conn subject event-type payload]
  (let [event (make-event event-type payload)
        data  (encode-event event)]
    (.publish conn subject data)
    event))

(defn subscribe!
  "Subscribe to a NATS subject (supports wildcards like chat.events.*).

   handler: (fn [event-map]) called on each message.
   Returns a Dispatcher that can be passed to unsubscribe!."
  [^Connection conn subject handler]
  (let [dispatcher (.createDispatcher conn
                     (reify MessageHandler
                       (onMessage [_ msg]
                         (try
                           (let [event (decode-event (.getData msg))]
                             (handler event))
                           (catch Exception e
                             (log/error e "Error handling NATS message on" subject))))))]
    (.subscribe dispatcher subject)
    dispatcher))

(defn unsubscribe!
  "Close a subscription dispatcher."
  [^Connection conn ^Dispatcher dispatcher]
  (.closeDispatcher conn dispatcher))

;;; ---------------------------------------------------------------------------
;;; JetStream
;;; ---------------------------------------------------------------------------

(defn ensure-stream!
  "Create or update the ajet-events JetStream stream."
  [^Connection conn]
  (let [js-mgmt (.jetStreamManagement conn)
        config  (-> (StreamConfiguration/builder)
                    (.name "ajet-events")
                    (.subjects (into-array String
                                 ["chat.events.>"
                                  "chat.dm.>"
                                  "chat.typing.>"
                                  "chat.presence.>"
                                  "chat.notifications.>"
                                  "chat.audit"]))
                    (.maxAge (Duration/ofHours 24))
                    (.maxBytes 1073741824) ;; 1GB
                    (.storageType StorageType/File)
                    (.replicas 1)
                    (.discardPolicy DiscardPolicy/Old)
                    (.build))]
    (try
      (.addStream js-mgmt config)
      (log/info "Created JetStream stream: ajet-events")
      (catch io.nats.client.JetStreamApiException e
        (if (= 10058 (.getApiErrorCode e)) ;; stream already exists
          (do
            (.updateStream js-mgmt config)
            (log/info "Updated JetStream stream: ajet-events"))
          (throw e))))))

(defn- build-push-subscribe-opts
  "Build PushSubscribeOptions via Method.invoke.
   SubscribeOptions$Builder is package-private so Clojure can't call its methods
   even with type hints (CLJ-1243). Method.invoke bypasses that restriction."
  [consumer-config stream-name]
  (let [builder (PushSubscribeOptions/builder)
        cls     (class builder)
        invoke! (fn [method-name arg-types args]
                  (.invoke (.getMethod cls method-name (into-array Class arg-types))
                           builder (object-array args)))]
    (invoke! "configuration" [ConsumerConfiguration] [consumer-config])
    (invoke! "stream" [String] [stream-name])
    (invoke! "build" [] [])))

(defn jetstream-subscribe!
  "Create a JetStream push subscription with a durable consumer.

   opts:
     :durable-name    - durable consumer name (e.g. \"sm-web-user-uuid\")
     :filter-subjects - seq of subjects to filter (e.g. [\"chat.events.community-id\"])
     :deliver-from    - Instant to deliver from (for reconnection)

   Returns a map {:subscription ... :dispatcher ...}.
   Clean up via (.closeDispatcher conn (:dispatcher result))."
  [^Connection conn subject handler
   {:keys [durable-name filter-subjects deliver-from]}]
  (let [js  (.jetStream conn)
        consumer-config (-> (cond-> (ConsumerConfiguration/builder)
                              durable-name   (.durable durable-name)
                              deliver-from   (.deliverPolicy DeliverPolicy/ByStartTime)
                              deliver-from   (.startTime (.atZone ^Instant deliver-from
                                                           (java.time.ZoneId/of "UTC")))
                              (not deliver-from) (.deliverPolicy DeliverPolicy/New))
                            (.build))
        push-opts  (build-push-subscribe-opts consumer-config "ajet-events")
        msg-handler (reify MessageHandler
                      (onMessage [_ msg]
                        (try
                          (let [event (decode-event (.getData msg))]
                            (handler event))
                          (catch Exception e
                            (log/error e "Error handling JetStream message")))))
        dispatcher (.createDispatcher conn msg-handler)
        subscription (.subscribe js subject dispatcher msg-handler true push-opts)]
    {:subscription subscription :dispatcher dispatcher}))

(defn js-publish!
  "Publish to JetStream (with ack). For critical events that must be durable."
  [^Connection conn subject event-type payload]
  (let [js    (.jetStream conn)
        event (make-event event-type payload)
        data  (encode-event event)]
    (.publish js subject data)
    event))
