(ns ajet.chat.cli.commands
  "All CLI command implementations.

   Every public function in this namespace follows the pattern:
     (fn [args opts] ...) -> exit-code (int)

   args: parsed positional arguments (vector of strings)
   opts: parsed options map (includes :json? flag)

   All API calls go through ajet.chat.shared.api-client.
   All output goes through ajet.chat.cli.output."
  (:require [clojure.string :as str]
            [ajet.chat.cli.config :as config]
            [ajet.chat.cli.output :as output]
            [ajet.chat.shared.api-client :as api])
  (:import [java.io BufferedReader InputStreamReader]))

;;; ---------------------------------------------------------------------------
;;; Helpers
;;; ---------------------------------------------------------------------------

(defn- resolve-community-id
  "Resolve a community slug or UUID to a community ID.
   If community-slug is nil, uses the default community from config/state.
   Returns the community map or throws."
  [ctx community-slug]
  (let [communities (:communities (api/get-communities ctx))
        slug        (or community-slug (config/get-default-community))]
    (when-not slug
      (throw (ex-info "No community specified"
                      {:type :ajet.chat/usage-error
                       :hint "Use --community <slug> or set a default with 'ajet config set default-community <slug>'"})))
    (let [match (first (filter #(or (= (:slug %) slug)
                                    (= (str (:id %)) slug))
                               communities))]
      (when-not match
        (throw (ex-info (str "Community not found: " slug)
                        {:type :ajet.chat/not-found
                         :hint "Run 'ajet communities' to see available communities"})))
      match)))

(defn- resolve-channel
  "Resolve a channel name to a channel map within a community.
   Returns the channel map or throws."
  [ctx community-id channel-name]
  (let [channels (:channels (api/get-channels ctx community-id))
        match    (first (filter #(or (= (:name %) channel-name)
                                     (= (str (:id %)) channel-name))
                                channels))]
    (when-not match
      (throw (ex-info (str "Channel not found: #" channel-name)
                      {:type :ajet.chat/not-found
                       :hint "Run 'ajet channels' to see available channels"})))
    match))

(defn- read-stdin
  "Read all of stdin and return as a string."
  []
  (let [reader (BufferedReader. (InputStreamReader. System/in))
        sb     (StringBuilder.)]
    (loop []
      (let [line (.readLine reader)]
        (when line
          (when (pos? (.length sb))
            (.append sb "\n"))
          (.append sb line)
          (recur))))
    (str sb)))

(defn- save-last-community!
  "Save the last-used community to state."
  [community-id]
  (let [state (config/load-state)]
    (config/save-state! (assoc state :last-community (str community-id)))))

(defn- save-last-channel!
  "Save the last-used channel for a community to state."
  [community-id channel-id]
  (let [state (config/load-state)]
    (config/save-state!
      (assoc-in state [:last-channels (str community-id)] (str channel-id)))))

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

(defn communities
  "List communities the current user belongs to."
  [_args {:keys [json?]}]
  (let [ctx    (config/make-ctx)
        result (api/get-communities ctx)
        comms  (or (:communities result) result)]
    (if json?
      (output/print-json result)
      (if (empty? comms)
        (output/print-info "You are not a member of any communities.")
        (output/print-table
          [["Name" :name] ["Slug" :slug] ["Role" :role] ["Members" :member-count]]
          comms)))
    0))

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

(defn channels
  "List, join, or leave channels in a community."
  [_args {:keys [json? community join leave]}]
  (let [ctx       (config/make-ctx)
        comm      (resolve-community-id ctx community)
        comm-id   (:id comm)]
    (save-last-community! comm-id)
    (cond
      join
      (let [ch (resolve-channel ctx comm-id join)]
        (api/join-channel ctx (:id ch))
        (if json?
          (output/print-json {:joined (:name ch)})
          (output/print-success (str "Joined #" (:name ch))))
        0)

      leave
      (let [ch (resolve-channel ctx comm-id leave)]
        (api/leave-channel ctx (:id ch))
        (if json?
          (output/print-json {:left (:name ch)})
          (output/print-success (str "Left #" (:name ch))))
        0)

      :else
      (let [result (api/get-channels ctx comm-id)
            chs    (or (:channels result) result)]
        (if json?
          (output/print-json result)
          (if (empty? chs)
            (output/print-info "No channels found.")
            (output/print-table
              [["Name" :name] ["Type" :type] ["Topic" :topic] ["Members" :member-count]]
              (map (fn [ch]
                     (-> ch
                         (update :name #(str "#" %))
                         (update :type #(or (some-> % name) "text"))
                         (update :topic #(or % ""))))
                   chs))))
        0))))

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

(defn read-messages
  "Read messages from a channel."
  [args {:keys [json? community limit before thread]}]
  (let [channel-name (first args)]
    (when-not channel-name
      (throw (ex-info "Channel name required"
                      {:type :ajet.chat/usage-error
                       :hint "Usage: ajet read <channel> [--limit N] [--before ID] [--thread ID]"})))
    (let [ctx      (config/make-ctx)
          comm     (resolve-community-id ctx community)
          comm-id  (:id comm)
          ch       (resolve-channel ctx comm-id channel-name)
          ch-id    (:id ch)]
      (save-last-community! comm-id)
      (save-last-channel! comm-id ch-id)
      (if thread
        ;; Read thread replies
        (let [result (api/get-thread ctx thread)]
          (if json?
            (output/print-json result)
            (let [messages (or (:messages result) result)]
              (output/print-info (str "Thread in #" (:name ch)))
              (output/print-info "")
              (output/print-messages messages))))
        ;; Read channel messages
        (let [opts   (cond-> {}
                       limit  (assoc :limit (parse-long (str limit)))
                       before (assoc :before before))
              result (api/get-messages ctx ch-id opts)]
          (if json?
            (output/print-json result)
            (let [messages (or (:messages result) result)]
              (output/print-messages messages
                {:channel-name (:name ch)
                 :channel-topic (:topic ch)})))))
      0)))

(defn send-message
  "Send a message to a channel."
  [args {:keys [json? community stdin image]}]
  (let [channel-name (first args)
        message-text (if stdin
                       (read-stdin)
                       (str/join " " (rest args)))]
    (when-not channel-name
      (throw (ex-info "Channel name required"
                      {:type :ajet.chat/usage-error
                       :hint "Usage: ajet send <channel> <message> [--stdin] [--image <path>]"})))
    (when (and (not stdin) (str/blank? message-text))
      (throw (ex-info "Message text required"
                      {:type :ajet.chat/usage-error
                       :hint "Usage: ajet send <channel> <message> or echo 'msg' | ajet send <channel> --stdin"})))
    (let [ctx      (config/make-ctx)
          comm     (resolve-community-id ctx community)
          comm-id  (:id comm)
          ch       (resolve-channel ctx comm-id channel-name)
          ch-id    (:id ch)]
      (save-last-community! comm-id)
      (save-last-channel! comm-id ch-id)
      ;; Upload image first if specified
      (when image
        (let [file (java.io.File. ^String image)]
          (when-not (.exists file)
            (throw (ex-info (str "File not found: " image)
                            {:type :ajet.chat/usage-error})))
          (let [ext (str/lower-case (or (last (str/split image #"\.")) ""))
                ct  (case ext
                      "png"  "image/png"
                      "jpg"  "image/jpeg"
                      "jpeg" "image/jpeg"
                      "gif"  "image/gif"
                      "webp" "image/webp"
                      (throw (ex-info (str "Unsupported image format: " ext)
                                      {:type :ajet.chat/usage-error
                                       :hint "Supported: png, jpg, jpeg, gif, webp"})))]
            (api/upload-file ctx ch-id image ct))))
      (let [body   {:body-md message-text}
            result (api/send-message ctx ch-id body)]
        (if json?
          (output/print-json result)
          (output/print-success (str "Message sent to #" (:name ch))))
        0))))

(defn edit-message
  "Edit a message by ID."
  [args {:keys [json?]}]
  (let [msg-id   (first args)
        new-text (str/join " " (rest args))]
    (when-not msg-id
      (throw (ex-info "Message ID required"
                      {:type :ajet.chat/usage-error
                       :hint "Usage: ajet edit <message-id> <new-text>"})))
    (when (str/blank? new-text)
      (throw (ex-info "New message text required"
                      {:type :ajet.chat/usage-error
                       :hint "Usage: ajet edit <message-id> <new-text>"})))
    (let [ctx    (config/make-ctx)
          result (api/edit-message ctx msg-id {:body-md new-text})]
      (if json?
        (output/print-json result)
        (output/print-success "Message edited"))
      0)))

(defn delete-message
  "Delete a message by ID (with confirmation)."
  [args {:keys [json? force]}]
  (let [msg-id (first args)]
    (when-not msg-id
      (throw (ex-info "Message ID required"
                      {:type :ajet.chat/usage-error
                       :hint "Usage: ajet delete <message-id>"})))
    (when (and (not json?) (not force))
      (when-not (output/confirm? "Are you sure you want to delete this message?")
        (output/print-info "Cancelled.")
        (throw (ex-info "Cancelled" {:type :ajet.chat/cancelled}))))
    (let [ctx    (config/make-ctx)
          result (api/delete-message ctx msg-id)]
      (if json?
        (output/print-json (or result {:deleted msg-id}))
        (output/print-success "Message deleted"))
      0)))

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

(defn list-dms
  "List DM channels."
  [_args {:keys [json?]}]
  (let [ctx    (config/make-ctx)
        result (api/get-dms ctx)
        dms    (or (:dms result) (:channels result) result)]
    (if json?
      (output/print-json result)
      (if (empty? dms)
        (output/print-info "No DM conversations.")
        (output/print-table
          [["User" :display-name] ["Username" :username] ["Last Message" :last-message-preview] ["Time" :last-message-at]]
          (map (fn [dm]
                 (-> dm
                     (update :display-name #(or % (:username dm) ""))
                     (update :username #(or % ""))
                     (update :last-message-preview #(or % ""))
                     (update :last-message-at #(if % (output/relative-time %) ""))))
               dms))))
    0))

(defn send-dm
  "Send a DM to a user by username."
  [args {:keys [json? read]}]
  (let [username (first args)]
    (when-not username
      (throw (ex-info "Username required"
                      {:type :ajet.chat/usage-error
                       :hint "Usage: ajet dm <username> <message> or ajet dm <username> --read"})))
    (let [ctx (config/make-ctx)]
      (if read
        ;; Read DM conversation
        (let [dm-result (api/create-dm ctx {:username username})
              ch-id     (or (:id dm-result) (:channel-id dm-result))
              messages  (api/get-messages ctx ch-id {:limit 50})]
          (if json?
            (output/print-json messages)
            (let [msgs (or (:messages messages) messages)]
              (output/print-info (str "DM with @" username))
              (output/print-info "")
              (output/print-messages msgs)))
          0)
        ;; Send DM
        (let [message-text (str/join " " (rest args))]
          (when (str/blank? message-text)
            (throw (ex-info "Message text required"
                            {:type :ajet.chat/usage-error
                             :hint "Usage: ajet dm <username> <message> or ajet dm <username> --read"})))
          (let [dm-result (api/create-dm ctx {:username username})
                ch-id     (or (:id dm-result) (:channel-id dm-result))
                result    (api/send-message ctx ch-id {:body-md message-text})]
            (if json?
              (output/print-json result)
              (output/print-success (str "DM sent to @" username)))
            0))))))

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

(defn notifications
  "List or manage notifications."
  [_args {:keys [json? all mark-read]}]
  (let [ctx (config/make-ctx)]
    (if mark-read
      ;; Mark all as read
      (let [result (api/mark-notifications-read ctx {:all true})]
        (if json?
          (output/print-json (or result {:marked-read true}))
          (output/print-success "All notifications marked as read"))
        0)
      ;; List notifications
      (let [opts   (if all {} {:unread true})
            result (api/get-notifications ctx opts)
            notifs (or (:notifications result) result)]
        (if json?
          (output/print-json result)
          (if (empty? notifs)
            (output/print-info (if all "No notifications." "No unread notifications."))
            (doseq [n notifs]
              (let [type-str (case (keyword (name (or (:type n) "")))
                               :mention      "@mention"
                               :dm           "DM"
                               :thread-reply "thread reply"
                               :invite       "invite"
                               (str (:type n)))
                    read?    (:read n)
                    marker   (if read? "  " "* ")
                    time-str (output/relative-time (:created-at n))]
                (output/print-info
                  (str marker type-str " - " (or (:preview n) (:source-id n))
                       "  " time-str))))))
        0))))

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

(defn search
  "Search messages and channels with filters."
  [args {:keys [json? community channel from type]}]
  (let [query (str/join " " args)]
    (when (str/blank? query)
      (throw (ex-info "Search query required"
                      {:type :ajet.chat/usage-error
                       :hint "Usage: ajet search <query> [--channel <ch>] [--from <user>] [--type messages|channels]"})))
    (let [ctx      (config/make-ctx)
          comm     (when (or community channel)
                     (resolve-community-id ctx community))
          comm-id  (when comm (:id comm))
          ch       (when (and channel comm-id)
                     (resolve-channel ctx comm-id channel))
          opts     (cond-> {:q query}
                     comm-id    (assoc :community-id comm-id)
                     ch         (assoc :channel-id (:id ch))
                     from       (assoc :from from)
                     type       (assoc :type (keyword type)))
          result   (api/search ctx opts)]
      (if json?
        (output/print-json result)
        (let [results (or (:results result) (:messages result) result)]
          (if (empty? results)
            (output/print-info "No results found.")
            (do
              (output/print-info (str "Search results for: " query))
              (output/print-info "")
              (output/print-messages results {:show-channel true})))))
      0)))

;;; ---------------------------------------------------------------------------
;;; Presence & Status
;;; ---------------------------------------------------------------------------

(defn set-status
  "Set or show the user's status text."
  [args {:keys [json?]}]
  (let [ctx        (config/make-ctx)
        status-text (str/join " " args)]
    (if (str/blank? status-text)
      ;; Show current status
      (let [me (api/get-me ctx)]
        (if json?
          (output/print-json {:status-text (:status-text me)})
          (output/print-info
            (if (:status-text me)
              (str "Status: " (:status-text me))
              "No status set.")))
        0)
      ;; Set status
      (let [result (api/update-me ctx {:status-text status-text})]
        (if json?
          (output/print-json result)
          (output/print-success (str "Status set to: " status-text)))
        0))))

(defn who-online
  "Show online users in the current community."
  [_args {:keys [json? community]}]
  (let [ctx     (config/make-ctx)
        comm    (resolve-community-id ctx community)
        comm-id (:id comm)
        result  (api/get-presence ctx comm-id)
        users   (or (:users result) (:online result) result)]
    (save-last-community! comm-id)
    (if json?
      (output/print-json result)
      (if (empty? users)
        (output/print-info "No users online.")
        (do
          (output/print-info (str "Online in " (:name comm) ":"))
          (output/print-info "")
          (output/print-table
            [["User" :display-name] ["Username" :username] ["Status" :status-text]]
            (map (fn [u]
                   (-> u
                       (update :display-name #(or % (:username u) ""))
                       (update :username #(or % ""))
                       (update :status-text #(or % ""))))
                 users)))))
    0))

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

(defn invite
  "Create, list, or revoke invites."
  [args {:keys [json? community max-uses]}]
  (let [subcommand (first args)]
    (case subcommand
      "create"
      (let [ctx     (config/make-ctx)
            comm    (resolve-community-id ctx community)
            comm-id (:id comm)
            body    (cond-> {}
                      max-uses (assoc :max-uses (parse-long (str max-uses))))
            result  (api/create-invite ctx comm-id body)]
        (save-last-community! comm-id)
        (if json?
          (output/print-json result)
          (let [code       (or (:code result) (:id result))
                server-url (config/get-server-url)]
            (output/print-success "Invite created")
            (output/print-info (str "  Link: " server-url "/invite/" code))
            (output/print-info (str "  Code: " code))
            (when max-uses
              (output/print-info (str "  Max uses: " max-uses)))))
        0)

      "list"
      (let [ctx     (config/make-ctx)
            comm    (resolve-community-id ctx community)
            comm-id (:id comm)
            result  (api/get-invites ctx comm-id)
            invites (or (:invites result) result)]
        (save-last-community! comm-id)
        (if json?
          (output/print-json result)
          (if (empty? invites)
            (output/print-info "No active invites.")
            (output/print-table
              [["Code" :code] ["Uses" :uses] ["Max" :max-uses] ["Created" :created-at] ["Expires" :expires-at]]
              (map (fn [inv]
                     (-> inv
                         (update :uses #(or (str %) "0"))
                         (update :max-uses #(if % (str %) "unlimited"))
                         (update :created-at #(if % (output/relative-time %) ""))
                         (update :expires-at #(if % (output/relative-time %) "never"))))
                   invites))))
        0)

      "revoke"
      (let [invite-id (second args)]
        (when-not invite-id
          (throw (ex-info "Invite ID required"
                          {:type :ajet.chat/usage-error
                           :hint "Usage: ajet invite revoke <invite-id>"})))
        (let [ctx    (config/make-ctx)
              result (api/revoke-invite ctx invite-id)]
          (if json?
            (output/print-json (or result {:revoked invite-id}))
            (output/print-success (str "Invite revoked: " invite-id)))
          0))

      ;; Default: unknown subcommand
      (throw (ex-info (str "Unknown invite subcommand: " (or subcommand ""))
                      {:type :ajet.chat/usage-error
                       :hint "Usage: ajet invite <create|list|revoke>"})))))

;;; ---------------------------------------------------------------------------
;;; Config
;;; ---------------------------------------------------------------------------

(defn config-cmd
  "Show or set configuration values."
  [args {:keys [json?]}]
  (let [subcommand (first args)]
    (case subcommand
      "set"
      (let [key-name (second args)
            value    (str/join " " (drop 2 args))]
        (when-not key-name
          (throw (ex-info "Config key required"
                          {:type :ajet.chat/usage-error
                           :hint "Usage: ajet config set <key> <value>"})))
        (when (str/blank? value)
          (throw (ex-info "Config value required"
                          {:type :ajet.chat/usage-error
                           :hint "Usage: ajet config set <key> <value>"})))
        (let [cfg     (config/load-config)
              kw-key  (keyword key-name)
              ;; Coerce value
              coerced (cond
                        (= "true" value)  true
                        (= "false" value) false
                        (re-matches #"\d+" value) (parse-long value)
                        :else value)
              new-cfg (assoc cfg kw-key coerced)]
          (config/save-config! new-cfg)
          (if json?
            (output/print-json {kw-key coerced})
            (output/print-success (str "Set " key-name " = " (pr-str coerced))))
          0))

      "server"
      (let [url (second args)]
        (when-not url
          (throw (ex-info "Server URL required"
                          {:type :ajet.chat/usage-error
                           :hint "Usage: ajet config server <url>"})))
        (let [cfg     (config/load-config)
              new-cfg (assoc cfg :server-url url)]
          (config/save-config! new-cfg)
          (if json?
            (output/print-json {:server-url url})
            (output/print-success (str "Server URL set to: " url)))
          0))

      ;; Default: show config
      (let [cfg (config/load-config)]
        (if json?
          (output/print-json cfg)
          (do
            (output/print-info "Current configuration:")
            (output/print-info "")
            (doseq [[k v] (sort-by key cfg)]
              (if (map? v)
                (do
                  (output/print-info (str "  " (name k) ":"))
                  (doseq [[k2 v2] (sort-by key v)]
                    (output/print-info (str "    " (name k2) ": " (pr-str v2)))))
                (output/print-info (str "  " (name k) ": " (pr-str v)))))))
        0))))
