(ns ajet.chat.cli.tui
  "TUI mode — full interactive terminal application using clojure-tui Elm architecture.

   Launched with `ajet tui`. Connects to the TUI session manager via SSE
   for real-time events, and uses the API client for data operations.

   Architecture: clojure-tui's Elm model (init/update/view) manages all state.
   SSE events arrive via a shared LinkedBlockingQueue polled every 100ms.
   API calls are dispatched as futures that write results back to the queue.

   Layout (4 panes via :col/:row):
     Header  — app name, community name, connection status, username
     Sidebar — communities, channels, DMs with unread counts
     Content — scrollable message list + typing indicator
     Input   — message composition line
     Status  — keybindings and focus indicator

   TODO: clojure-tui gaps — features that cannot be implemented with
   the current library (see cli/README.md for full details):

   1. Mouse support (PRD 4.4) — no mouse tracking/parsing in clojure-tui
   2. Inline image rendering (PRD 4.5) — no timg/sixel/kitty support
   3. Multiline text input (PRD 4.4) — :input widget is single-line only
   4. Autocomplete dropdowns (PRD 4.4) — no @mention/#channel//cmd popups
   5. SSE client integration (PRD 4.7) — workaround: external queue + polling
   6. Terminal bell (PRD 4.8) — trivial but outside render model
   7. OSC 8 hyperlinks (PRD 4.6) — no OSC 8 in tui.ansi
   8. Spoiler text reveal (PRD 4.6) — needs per-message hidden state"
  (:require [tui.core :as tui]
            [tui.events :as ev]
            [tui.ansi :as ansi]
            [babashka.http-client :as http]
            [clojure.data.json :as json]
            [clojure.string :as str]
            [ajet.chat.cli.config :as config]
            [ajet.chat.shared.api-client :as api]
            [ajet.chat.shared.markdown :as markdown]
            [ajet.chat.shared.mentions :as mentions])
  (:import [java.io BufferedReader InputStreamReader]
           [java.net HttpURLConnection URL]
           [java.util ArrayList]
           [java.util.concurrent LinkedBlockingQueue]))

;;; ---------------------------------------------------------------------------
;;; SSE parsing (pure)
;;; ---------------------------------------------------------------------------

(defn- parse-sse-event
  "Parse accumulated SSE lines into {:event type :data parsed-json}."
  [lines]
  (when (seq lines)
    (let [result (reduce
                   (fn [acc line]
                     (cond
                       (str/starts-with? line "event:")
                       (assoc acc :event (str/trim (subs line 6)))
                       (str/starts-with? line "data:")
                       (update acc :data-lines conj (str/trim (subs line 5)))
                       (str/starts-with? line "id:")
                       (assoc acc :id (str/trim (subs line 3)))
                       :else acc))
                   {:event "message" :data-lines [] :id nil}
                   lines)
          data-str (str/join "\n" (:data-lines result))]
      (when-not (str/blank? data-str)
        {:event (:event result)
         :data  (try (json/read-str data-str :key-fn keyword)
                     (catch Exception _ {:raw data-str}))
         :id    (:id result)}))))

;;; ---------------------------------------------------------------------------
;;; SSE background thread (writes to shared queue)
;;; ---------------------------------------------------------------------------

(defn- connect-sse
  "Open SSE connection. Returns {:connection :reader} or nil."
  [base-url token community-id]
  (let [url-str (str base-url "/tui/sse/events"
                     (when community-id
                       (str "?community_id=" community-id)))]
    (try
      (let [url  (URL. url-str)
            conn ^HttpURLConnection (.openConnection url)]
        (.setRequestMethod conn "GET")
        (.setRequestProperty conn "Accept" "text/event-stream")
        (.setRequestProperty conn "Authorization" (str "Bearer " token))
        (.setRequestProperty conn "Cache-Control" "no-cache")
        (.setConnectTimeout conn 10000)
        (.setReadTimeout conn 0)
        (.connect conn)
        (when (= 200 (.getResponseCode conn))
          {:connection conn
           :reader     (BufferedReader.
                         (InputStreamReader. (.getInputStream conn)))}))
      (catch Exception _ nil))))

(defn- sse-read-loop!
  "Read SSE stream, put parsed events on queue. Blocks until disconnect."
  [^BufferedReader reader ^LinkedBlockingQueue queue running?]
  (try
    (loop [lines []]
      (when @running?
        (let [line (.readLine reader)]
          (if (nil? line)
            (.put queue {:type :sse :event "disconnected" :data {}})
            (if (str/blank? line)
              (do
                (when-let [event (parse-sse-event lines)]
                  (.put queue {:type :sse
                               :event (:event event)
                               :data  (:data event)}))
                (recur []))
              (recur (conj lines line)))))))
    (catch Exception _
      (.put queue {:type :sse :event "disconnected" :data {}}))))

(defn- start-sse-thread!
  "Start background SSE connection manager with auto-reconnect."
  [base-url token ^LinkedBlockingQueue queue running?]
  (future
    (loop [backoff 1000]
      (when @running?
        (let [conn-info (connect-sse base-url token nil)]
          (if conn-info
            (do
              (.put queue {:type :sse-status :connected true :reconnecting false})
              (sse-read-loop! (:reader conn-info) queue running?)
              (try (.close ^java.io.Closeable (:reader conn-info))
                   (catch Exception _ nil))
              (when @running?
                (.put queue {:type :sse-status :connected false :reconnecting true})
                (Thread/sleep 1000)
                (recur 1000)))
            (do
              (.put queue {:type :sse-status :connected false :reconnecting true})
              (Thread/sleep (min backoff 30000))
              (recur (min (* backoff 2) 30000)))))))))

;;; ---------------------------------------------------------------------------
;;; Side-effect dispatchers (futures that write results to queue)
;;; ---------------------------------------------------------------------------

(defn- fire-load-data!
  "Load communities, channels, messages in background."
  [ctx ^LinkedBlockingQueue queue {:keys [community channel]}]
  (future
    (try
      (let [saved    (config/load-state)
            result   (api/get-communities ctx)
            comms    (vec (or (:communities result) result))
            ;; Resolve community
            comm-id  (or (when community
                           (some #(when (or (= (:slug %) community)
                                           (= (str (:id %)) community))
                                    (str (:id %)))
                                 comms))
                         (:last-community saved)
                         (when (seq comms) (str (:id (first comms)))))]
        (if comm-id
          (let [ch-result (api/get-channels ctx comm-id)
                chs       (vec (or (:channels ch-result) ch-result))
                ;; Resolve channel
                ch-id     (or (when channel
                                (some #(when (= (:name %) channel)
                                         (str (:id %)))
                                      chs))
                              (get-in saved [:last-channels comm-id])
                              (when (seq chs) (str (:id (first chs)))))
                msgs      (when ch-id
                            (let [r (api/get-messages ctx ch-id {:limit 50})]
                              (vec (or (:messages r) r))))]
            (.put queue {:type             :data-loaded
                         :communities      comms
                         :channels         chs
                         :active-community comm-id
                         :active-channel   ch-id
                         :messages         (or msgs [])}))
          (.put queue {:type             :data-loaded
                       :communities      comms
                       :channels         []
                       :active-community nil
                       :active-channel   nil
                       :messages         []})))
      (catch Exception e
        (.put queue {:type :data-error :error (.getMessage e)})))))

(defn- fire-switch-channel!
  "Load messages for a channel + notify TUI SM."
  [ctx ch-id ^LinkedBlockingQueue queue]
  (future
    (try
      (let [result (api/get-messages ctx ch-id {:limit 50})
            msgs   (vec (or (:messages result) result))]
        (.put queue {:type :channel-messages :channel-id ch-id :messages msgs}))
      (catch Exception _ nil))
    (try
      (http/request
        {:method  :post
         :uri     (str (:base-url ctx) "/tui/navigate")
         :headers {"Authorization" (str "Bearer " (:auth-token ctx))
                   "Content-Type"  "application/json"}
         :body    (json/write-str {:channel-id ch-id})
         :throw   false})
      (catch Exception _ nil))))

(defn- fire-send-message!
  [ctx channel-id text ^LinkedBlockingQueue queue]
  (future
    (try
      (api/send-message ctx channel-id {:body-md text})
      (catch Exception e
        (.put queue {:type :send-error :error (.getMessage e)})))))

(defn- fire-load-older!
  [ctx channel-id before-id ^LinkedBlockingQueue queue]
  (future
    (try
      (let [result (api/get-messages ctx channel-id {:limit 50 :before before-id})
            msgs   (vec (or (:messages result) result))]
        (when (seq msgs)
          (.put queue {:type :older-messages :messages msgs})))
      (catch Exception _ nil))))

;;; ---------------------------------------------------------------------------
;;; Queue draining
;;; ---------------------------------------------------------------------------

(defn- drain-queue
  "Drain all available events from queue into a vector."
  [^LinkedBlockingQueue queue]
  (let [buf (ArrayList.)]
    (.drainTo queue buf)
    (vec buf)))

;;; ---------------------------------------------------------------------------
;;; Model
;;; ---------------------------------------------------------------------------

(defn- initial-model
  [ctx session ^LinkedBlockingQueue queue overrides]
  {:ctx       ctx
   :session   session
   :queue     queue
   :overrides overrides   ;; {:community ... :channel ...} for initial load

   :connected    false
   :reconnecting false

   ;; Data
   :communities  []
   :channels     []
   :messages     []
   :typing-users #{}
   :unread-counts {}

   ;; Navigation
   :active-community nil
   :active-channel   nil

   ;; Input — TODO: Multiline input (PRD 4.4) — :input is single-line only
   :input-text ""

   ;; Focus: :input | :messages | :sidebar
   :focus :input

   ;; Layout
   :sidebar-width 22
   :scroll-offset 0

   ;; User
   :username (:username session)
   :user-id  (:user-id session)

   ;; Errors
   :error-message nil
   :loading       false})

;;; ---------------------------------------------------------------------------
;;; Pure model helpers
;;; ---------------------------------------------------------------------------

(defn- community-name [model]
  (or (some #(when (= (str (:id %)) (str (:active-community model)))
               (:name %))
            (:communities model))
      "ajet chat"))

(defn- channel-name [model]
  (or (some #(when (= (str (:id %)) (str (:active-channel model)))
               (:name %))
            (:channels model))
      "?"))

(defn- channel-ids [model]
  (mapv #(str (:id %)) (:channels model)))

(defn- text-channels [model]
  (filterv #(or (= (:type %) "text") (= (:type %) :text) (nil? (:type %)))
           (:channels model)))

(defn- dm-channels [model]
  (filterv #(or (= (:type %) "dm") (= (:type %) :dm)
                (= (:type %) "group-dm") (= (:type %) :group-dm))
           (:channels model)))

(defn- adjacent-channel [model direction]
  (let [ids (channel-ids model)
        idx (.indexOf ^java.util.List ids (str (:active-channel model)))]
    (when (seq ids)
      (nth ids (case direction
                 :next (if (< idx (dec (count ids))) (inc idx) 0)
                 :prev (if (pos? idx) (dec idx) (dec (count ids))))))))

;;; ---------------------------------------------------------------------------
;;; SSE event → model (pure)
;;; ---------------------------------------------------------------------------

(defn- apply-sse-event
  "Apply a single SSE event to the model. Returns {:model m :events []}."
  [model sse-event]
  (let [event-name (:event sse-event)
        data       (:data sse-event)]
    (case event-name
      "init"
      {:model (merge model
                {:communities      (or (:communities data) (:communities model))
                 :channels         (or (:channels data) (:channels model))
                 :active-community (or (:active-community data) (:active-community model))
                 :active-channel   (or (:active-channel data) (:active-channel model))
                 :unread-counts    (or (:unread-counts data) (:unread-counts model))})
       :events []}

      "message"
      (let [ch-id (or (:channel-id data) (:channel_id data))]
        ;; TODO: Terminal bell (PRD 4.8) — emit \a on @mention or DM
        {:model (if (= ch-id (:active-channel model))
                  (update model :messages conj data)
                  (update-in model [:unread-counts (str ch-id)] (fnil inc 0)))
         :events []})

      "message.update"
      (let [msg-id (or (:id data) (:message-id data))]
        {:model (update model :messages
                  (fn [msgs] (mapv #(if (= (:id %) msg-id) (merge % data) %) msgs)))
         :events []})

      "message.delete"
      (let [msg-id (or (:id data) (:message-id data))]
        {:model (update model :messages
                  (fn [msgs] (filterv #(not= (:id %) msg-id) msgs)))
         :events []})

      "typing"
      (let [username (or (:username data) (:user data))]
        (if (and username (not= username (:username model)))
          {:model  (update model :typing-users conj username)
           :events [(ev/delayed-event 5000 {:type :clear-typing :username username})]}
          {:model model :events []}))

      "channel.update"
      {:model (update model :channels
                (fn [chs] (mapv #(if (= (:id %) (:id data)) (merge % data) %) chs)))
       :events []}

      ;; Unknown / disconnected — ignore
      {:model model :events []})))

;;; ---------------------------------------------------------------------------
;;; Queue event → model (pure, except for typing events)
;;; ---------------------------------------------------------------------------

(defn- process-queue-event
  "Process a single queue event. Returns {:model m :events []}."
  [model event]
  (case (:type event)
    :sse-status
    {:model  (assoc model :connected (:connected event) :reconnecting (:reconnecting event))
     :events []}

    :sse
    (apply-sse-event model event)

    :data-loaded
    {:model  (merge model (select-keys event [:communities :channels :active-community
                                              :active-channel :messages])
                    {:loading false})
     :events []}

    :data-error
    {:model  (assoc model :error-message (:error event) :loading false)
     :events []}

    :channel-messages
    {:model  (if (= (:channel-id event) (:active-channel model))
               (assoc model :messages (:messages event))
               model)
     :events []}

    :older-messages
    {:model  (update model :messages #(vec (concat (:messages event) %)))
     :events []}

    :send-error
    {:model  (assoc model :error-message (:error event))
     :events []}

    ;; Unknown
    {:model model :events []}))

;;; ---------------------------------------------------------------------------
;;; Update function (Elm architecture)
;;; ---------------------------------------------------------------------------

(defn- tui-update
  [{:keys [model event]}]
  (let [queue ^LinkedBlockingQueue (:queue model)
        ctx   (:ctx model)]
    (cond
      ;; ── Poll SSE queue ──────────────────────────────────────────────
      (= (:type event) :sse-poll)
      (let [q-events     (drain-queue queue)
            {:keys [m extra-events]}
            (reduce (fn [{:keys [m extra-events]} e]
                      (let [result (process-queue-event m e)]
                        {:m            (:model result)
                         :extra-events (into extra-events (:events result))}))
                    {:m model :extra-events []}
                    q-events)]
        {:model  m
         :events (into [(ev/delayed-event 100 {:type :sse-poll})]
                       extra-events)})

      ;; ── Load initial data ───────────────────────────────────────────
      (= (:type event) :load-initial-data)
      (do
        (fire-load-data! ctx queue (:overrides model))
        {:model (assoc model :loading true)})

      ;; ── Clear typing indicator ──────────────────────────────────────
      (= (:type event) :clear-typing)
      {:model (update model :typing-users disj (:username event))}

      ;; ── Ctrl+Q / Ctrl+C → quit ─────────────────────────────────────
      (or (ev/key= event \q #{:ctrl})
          (ev/key= event \c #{:ctrl}))
      {:model model :events [(ev/quit)]}

      ;; ── Ctrl+K → search (placeholder) ──────────────────────────────
      (ev/key= event \k #{:ctrl})
      {:model model}

      ;; ── Ctrl+N → next channel ──────────────────────────────────────
      (ev/key= event \n #{:ctrl})
      (if-let [ch (adjacent-channel model :next)]
        (do
          (fire-switch-channel! ctx ch queue)
          {:model (assoc model
                    :active-channel ch
                    :messages       []
                    :scroll-offset  0
                    :typing-users   #{})})
        {:model model})

      ;; ── Ctrl+P → previous channel ──────────────────────────────────
      (ev/key= event \p #{:ctrl})
      (if-let [ch (adjacent-channel model :prev)]
        (do
          (fire-switch-channel! ctx ch queue)
          {:model (assoc model
                    :active-channel ch
                    :messages       []
                    :scroll-offset  0
                    :typing-users   #{})})
        {:model model})

      ;; ── Ctrl+E → edit (placeholder) ────────────────────────────────
      (ev/key= event \e #{:ctrl})
      {:model model}

      ;; ── Ctrl+D → delete (placeholder) ──────────────────────────────
      (ev/key= event \d #{:ctrl})
      {:model model}

      ;; ── Ctrl+R → react (placeholder) ───────────────────────────────
      (ev/key= event \r #{:ctrl})
      {:model model}

      ;; ── Ctrl+T → thread (placeholder) ──────────────────────────────
      (ev/key= event \t #{:ctrl})
      {:model model}

      ;; ── Tab → cycle focus ──────────────────────────────────────────
      (ev/key= event :tab)
      {:model (update model :focus
                {:input :messages, :messages :sidebar, :sidebar :input})}

      ;; ── Enter → send message ───────────────────────────────────────
      (ev/key= event :enter)
      (if (and (= (:focus model) :input)
               (not (str/blank? (:input-text model))))
        (do
          (fire-send-message! ctx (:active-channel model)
                              (:input-text model) queue)
          {:model (assoc model :input-text "" :error-message nil)})
        {:model model})

      ;; ── Backspace ──────────────────────────────────────────────────
      (ev/key= event :backspace)
      (if (and (= (:focus model) :input)
               (pos? (count (:input-text model))))
        {:model (update model :input-text #(subs % 0 (dec (count %))))}
        {:model model})

      ;; ── Arrow up ───────────────────────────────────────────────────
      (ev/key= event :up)
      {:model (if (= (:focus model) :messages)
                (update model :scroll-offset
                  #(min (inc %) (count (:messages model))))
                model)}

      ;; ── Arrow down ─────────────────────────────────────────────────
      (ev/key= event :down)
      {:model (if (= (:focus model) :messages)
                (update model :scroll-offset #(max 0 (dec %)))
                model)}

      ;; ── Page Up ────────────────────────────────────────────────────
      (ev/key= event :page-up)
      (let [new-model (update model :scroll-offset
                        #(min (+ % 10) (count (:messages model))))]
        (when (and (>= (:scroll-offset new-model) (count (:messages new-model)))
                   (:active-channel new-model)
                   (seq (:messages new-model)))
          (fire-load-older! ctx (:active-channel new-model)
                            (:id (first (:messages new-model))) queue))
        {:model new-model})

      ;; ── Page Down ──────────────────────────────────────────────────
      (ev/key= event :page-down)
      {:model (update model :scroll-offset #(max 0 (- % 10)))}

      ;; ── Escape → focus input ───────────────────────────────────────
      (ev/key= event :escape)
      {:model (assoc model :focus :input)}

      ;; ── j/k vim navigation in messages ─────────────────────────────
      (and (= (:focus model) :messages) (ev/key= event \j))
      {:model (update model :scroll-offset #(max 0 (dec %)))}

      (and (= (:focus model) :messages) (ev/key= event \k))
      {:model (update model :scroll-offset
                #(min (inc %) (count (:messages model))))}

      ;; ── Regular character → input ──────────────────────────────────
      (and (= (:type event) :key)
           (= (:focus model) :input)
           (char? (:key event))
           (<= 32 (int (:key event)) 126))
      {:model (update model :input-text str (:key event))}

      ;; ── Default → ignore ───────────────────────────────────────────
      :else
      {:model model})))

;;; ---------------------------------------------------------------------------
;;; View functions (pure hiccup → clojure-tui render primitives)
;;; ---------------------------------------------------------------------------

(defn- view-header
  "Header bar: app name | community name ... username status"
  [model]
  (let [comm  (community-name model)
        user  (:username model)
        [status-color status-text]
        (cond
          (:reconnecting model) [:yellow "reconnecting..."]
          (:connected model)    [:green "connected"]
          :else                 [:red "disconnected"])]
    [:row {:widths [:flex :flex]}
     [:text {:bg :blue :fg :white :bold true}
      (str " ajet chat | " comm " ")]
     [:text {:bg :blue :fg :white}
      (str user " ")
      [:text {:fg status-color} status-text]
      " "]]))

(defn- view-sidebar-entry
  "Single channel/DM entry in sidebar."
  [ch active-channel unread-counts sw]
  (let [ch-id    (str (:id ch))
        active?  (= ch-id (str active-channel))
        unread   (get unread-counts ch-id 0)
        is-dm?   (or (= (:type ch) "dm") (= (:type ch) :dm)
                     (= (:type ch) "group-dm") (= (:type ch) :group-dm))
        prefix   (if is-dm? "" "#")
        raw-name (str prefix (or (:name ch) (:display-name ch) "?"))
        name-str (ansi/truncate raw-name (- sw 6))]
    (if active?
      [:text {:bold true :fg :cyan}
       "> " name-str
       (when (pos? unread) [:text {:fg :yellow} (str " " unread)])]
      [:text {}
       "  " name-str
       (when (pos? unread) [:text {:fg :yellow} (str " " unread)])])))

(defn- view-sidebar [model]
  (let [{:keys [active-channel unread-counts sidebar-width]} model
        tchs (text-channels model)
        dchs (dm-channels model)]
    (into [:col {}
           [:text {:bold true :fg :white} "CHANNELS"]]
          (concat
            (for [ch tchs]
              (view-sidebar-entry ch active-channel unread-counts sidebar-width))
            (when (seq dchs)
              (cons [:text {} ""]
                    (cons [:text {:bold true :fg :white} "DMs"]
                          (for [ch dchs]
                            (view-sidebar-entry ch active-channel
                                               unread-counts sidebar-width)))))))))

(defn- format-relative-time
  "Simple relative time formatter."
  [ts]
  (if (nil? ts) ""
    (try
      (let [s (str ts)]
        (if (> (count s) 16) (subs s 11 16) s))
      (catch Exception _ ""))))

(defn- view-message
  "Single message: author + timestamp, then body lines."
  [msg]
  ;; TODO: Inline images (PRD 4.5) — show [image: file.png] placeholder only
  ;; TODO: Spoiler reveal (PRD 4.6) — ||spoiler|| not hidden yet
  ;; TODO: OSC 8 hyperlinks (PRD 4.6) — URLs not clickable
  (let [author   (or (:display-name msg) (:username msg) (:user-id msg) "?")
        time-str (format-relative-time (or (:created-at msg) (:timestamp msg)))
        body     (-> (or (:body-md msg) (:body msg) "")
                     (mentions/render (fn [_type id] id))
                     markdown/->ansi)
        lines    (str/split-lines body)]
    (into [:col {}
           [:text {}
            [:text {:bold true} author]
            "  "
            [:text {:fg :bright-black} time-str]
            (when (:edited-at msg)
              [:text {:fg :bright-black} " (edited)"])]]
          (for [line lines]
            [:text {} (str "  " line)]))))

(defn- view-messages
  "Message list with scroll offset."
  [model]
  (let [{:keys [messages scroll-offset]} model
        n       (count messages)
        visible (if (and (pos? scroll-offset) (> n 0))
                  (let [end   (max 0 (- n scroll-offset))
                        start (max 0 (- end 50))]
                    (subvec (vec messages) start end))
                  messages)]
    (if (seq visible)
      (into [:col {}]
            (for [msg visible]
              (view-message msg)))
      [:text {:fg :bright-black :italic true} "  No messages yet"])))

(defn- view-typing [model]
  (let [{:keys [typing-users]} model]
    (if (seq typing-users)
      [:text {:fg :bright-black :italic true}
       (str "  " (str/join ", " typing-users) " "
            (if (= 1 (count typing-users)) "is" "are")
            " typing...")]
      [:text {} ""])))

;; TODO: Autocomplete dropdowns (PRD 4.4) — no @mention/#channel//cmd popups
;; TODO: Multiline input (PRD 4.4) — :input is single-line, no Shift+Enter
(defn- view-input [model]
  (let [ch      (channel-name model)
        active? (= (:focus model) :input)]
    [:row {:widths [nil :flex]}
     [:text (if active? {:fg :cyan} {:fg :bright-black})
      (str (if active? "> " "  ") "#" ch " ")]
     [:input {:value       (:input-text model)
              :placeholder (when active? "Type a message...")}]]))

(defn- view-status-bar [model]
  (let [focus-str (str "[" (name (:focus model)) "]")
        keys-str  "Ctrl+Q:quit  Ctrl+K:search  Ctrl+N/P:next/prev  Tab:focus  Enter:send"
        err       (:error-message model)]
    [:text {:bg :bright-black :fg :white}
     (str " " focus-str " " keys-str
          (when err (str "  | Error: " err)))]))

(defn- tui-view
  "Root view: header / (sidebar | content) / input / status"
  [model]
  [:col {:heights [1 :flex 1 1]}
   (view-header model)
   [:row {:widths [(:sidebar-width model) :flex] :gap 1}
    (view-sidebar model)
    [:col {:heights [:flex 1]}
     (view-messages model)
     (view-typing model)]]
   (view-input model)
   (view-status-bar model)])

;;; ---------------------------------------------------------------------------
;;; Launch
;;; ---------------------------------------------------------------------------

(defn launch!
  "Launch the TUI using clojure-tui's Elm architecture.

   Options:
     :community - community slug to open to
     :channel   - channel name to open to

   Returns exit code."
  [& [{:keys [community channel]}]]
  (let [session (config/load-session)]
    (when-not session
      (throw (ex-info "Not logged in"
                      {:type :ajet.chat/auth-error
                       :hint "Run 'ajet login' to sign in"})))
    (let [ctx         (config/make-ctx)
          queue       (LinkedBlockingQueue.)
          sse-running (atom true)
          _           (start-sse-thread! (:base-url ctx)
                                         (:auth-token ctx)
                                         queue sse-running)
          init-model  (initial-model ctx session queue
                                     {:community community :channel channel})
          final-model (tui/run
                        {:init       init-model
                         :update     tui-update
                         :view       tui-view
                         :init-events [{:type :load-initial-data}
                                       (ev/delayed-event 100 {:type :sse-poll})]})]
      ;; Cleanup
      (reset! sse-running false)
      (config/save-state!
        {:last-community (str (:active-community final-model))
         :last-channels  {(str (:active-community final-model))
                          (str (:active-channel final-model))}})
      0)))
