(ns ajet.chat.cli.auth
  "Authentication commands for the CLI client.

   Supports two login modes:
   - Interactive OAuth: opens browser, starts temp HTTP callback server
   - Token: saves an API token directly for scripting

   Login flow:
   1. Start a temporary local HTTP server on a random port
   2. Open browser to auth gateway login with redirect to localhost callback
   3. Capture session token from callback query params
   4. Save to session.edn
   5. Fetch /api/me to confirm and print user info

   Fallback: if browser cannot open, print URL and prompt for manual token paste."
  (: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.net ServerSocket URLDecoder]
           [java.io BufferedReader InputStreamReader PrintWriter]
           [java.time Instant Duration]))

;;; ---------------------------------------------------------------------------
;;; Local callback server
;;; ---------------------------------------------------------------------------

(defn- parse-query-params
  "Parse query string into a map."
  [query-string]
  (when (and query-string (not (str/blank? query-string)))
    (into {}
      (map (fn [pair]
             (let [parts (str/split pair #"=" 2)]
               [(URLDecoder/decode (first parts) "UTF-8")
                (when (second parts)
                  (URLDecoder/decode (second parts) "UTF-8"))]))
           (str/split query-string #"&")))))

(defn- extract-request-info
  "Extract method, path, and query params from an HTTP request line."
  [request-line]
  (when request-line
    (let [parts    (str/split request-line #"\s+")
          method   (first parts)
          full-path (second parts)
          [path query] (str/split (or full-path "/") #"\?" 2)]
      {:method method
       :path   path
       :query  (parse-query-params query)})))

(defn- send-http-response
  "Send a simple HTTP response on a PrintWriter."
  [^PrintWriter writer status-code body]
  (.println writer (str "HTTP/1.1 " status-code " OK"))
  (.println writer "Content-Type: text/html; charset=utf-8")
  (.println writer "Connection: close")
  (.println writer (str "Content-Length: " (count (.getBytes ^String body "UTF-8"))))
  (.println writer "")
  (.print writer body)
  (.flush writer))

(def ^:private success-page
  "<html><body style=\"font-family:sans-serif;text-align:center;padding:60px;\">
   <h1>Logged in!</h1>
   <p>You can close this tab and return to your terminal.</p>
   </body></html>")

(def ^:private error-page
  "<html><body style=\"font-family:sans-serif;text-align:center;padding:60px;\">
   <h1>Login failed</h1>
   <p>Something went wrong. Please try again.</p>
   </body></html>")

(defn- start-callback-server
  "Start a temporary HTTP server on a random port. Blocks until a callback
   is received or timeout (120s). Returns the captured query params or nil."
  []
  (let [server   (ServerSocket. 0)
        port     (.getLocalPort server)
        result   (promise)]
    (.setSoTimeout server 120000) ;; 2 minute timeout
    (future
      (try
        (let [socket (.accept server)
              reader (BufferedReader. (InputStreamReader. (.getInputStream socket)))
              writer (PrintWriter. (.getOutputStream socket) true)
              line   (.readLine reader)
              info   (extract-request-info line)]
          (if (and info (get-in info [:query "token"]))
            (do
              (send-http-response writer 200 success-page)
              (deliver result (:query info)))
            (do
              (send-http-response writer 400 error-page)
              (deliver result nil)))
          (.close socket))
        (catch java.net.SocketTimeoutException _
          (deliver result nil))
        (catch Exception _e
          (deliver result nil))
        (finally
          (.close server))))
    {:port port :result result}))

;;; ---------------------------------------------------------------------------
;;; Browser opening
;;; ---------------------------------------------------------------------------

(defn- open-browser
  "Attempt to open a URL in the default browser. Returns true on success."
  [url]
  (try
    (let [os-name (str/lower-case (System/getProperty "os.name"))]
      (cond
        (str/includes? os-name "linux")
        (do (.start (ProcessBuilder. ["xdg-open" url])) true)

        (str/includes? os-name "mac")
        (do (.start (ProcessBuilder. ["open" url])) true)

        (str/includes? os-name "windows")
        (do (.start (ProcessBuilder. ["cmd" "/c" "start" url])) true)

        :else false))
    (catch Exception _
      false)))

;;; ---------------------------------------------------------------------------
;;; Login commands
;;; ---------------------------------------------------------------------------

(defn login-interactive
  "Interactive OAuth login flow.

   1. Starts a temporary local HTTP server on a random port
   2. Opens browser to auth gateway login URL
   3. Waits for callback with session token
   4. Saves session and fetches user info"
  [& [{:keys [json?]}]]
  (let [server-url (config/get-server-url)
        {:keys [port result]} (start-callback-server)
        callback-url (str "http://localhost:" port "/callback")
        login-url    (str server-url "/auth/login?redirect=" callback-url "&cli=true")
        browser-ok?  (open-browser login-url)]

    (if browser-ok?
      (output/print-info "Opening browser for login...")
      (output/print-info (str "Open this URL in your browser:\n  " login-url)))

    (output/print-info "\nIf your browser didn't open, visit:")
    (output/print-info (str "  " login-url))
    (output/print-info "\nOr paste your session token below:")
    (output/print-info "(Waiting for callback...)")

    ;; Start a parallel thread to accept manual token paste
    (let [manual-token (promise)]
      (future
        (try
          (when-let [line (read-line)]
            (when-not (str/blank? line)
              (deliver manual-token (str/trim line))))
          (catch Exception _ nil)))

      ;; Wait for either callback or manual paste
      (let [callback-result (deref result 120000 nil)
            token           (or (get callback-result "token")
                                (deref manual-token 100 nil))]
        (if-not token
          (do
            (output/print-error "Login timed out"
                                "No callback received within 2 minutes."
                                "Try 'ajet login --token <token>' instead")
            3)
          ;; Got a token - save session and verify
          (let [expires-at (or (get callback-result "expires_at")
                               (str (.plus (Instant/now) (Duration/ofDays 30))))
                user-id    (get callback-result "user_id")
                username   (get callback-result "username")]
            ;; Save initial session
            (config/save-session! {:token      token
                                   :user-id    user-id
                                   :username   username
                                   :expires-at expires-at})
            ;; Try to verify by fetching /api/me
            (try
              (let [ctx  (config/make-ctx)
                    me   (api/get-me ctx)]
                (config/save-session! {:token      token
                                       :user-id    (or (:id me) user-id)
                                       :username   (or (:username me) username)
                                       :expires-at expires-at})
                (if json?
                  (output/print-json me)
                  (output/print-success
                    (str "Logged in as "
                         (or (:display-name me) (:username me))
                         " (" (:username me) ")")))
                0)
              (catch Exception _
                ;; Token saved but couldn't verify - that's OK
                (if json?
                  (output/print-json {:token token :username username})
                  (output/print-success
                    (str "Logged in" (when username (str " as " username)))))
                0))))))))

(defn login-token
  "Login with an API token directly (for scripting).

   Saves the token and verifies it by calling /api/me."
  [token & [{:keys [json?]}]]
  (config/save-session! {:token      token
                         :user-id    nil
                         :username   nil
                         :expires-at (str (.plus (Instant/now) (Duration/ofDays 365)))})
  (try
    (let [ctx (config/make-ctx)
          me  (api/get-me ctx)]
      (config/save-session! {:token      token
                             :user-id    (:id me)
                             :username   (:username me)
                             :expires-at (str (.plus (Instant/now) (Duration/ofDays 365)))})
      (if json?
        (output/print-json me)
        (output/print-success
          (str "Logged in as "
               (or (:display-name me) (:username me))
               " (" (:username me) ")")))
      0)
    (catch Exception e
      (let [data (ex-data e)]
        (if (= :ajet.chat/api-error (:type data))
          (do
            (output/print-error "Invalid token"
                                "The provided token was rejected by the server."
                                "Check the token and try again")
            (config/clear-session!)
            3)
          (do
            (output/print-error "Could not verify token"
                                (.getMessage e)
                                "Token saved, but server may be unreachable")
            0))))))

(defn logout
  "Clear the current session."
  [& [{:keys [json?]}]]
  (config/clear-session!)
  (if json?
    (output/print-json {:status "logged_out"})
    (output/print-success "Logged out"))
  0)

(defn whoami
  "Fetch and display current user info."
  [& [{:keys [json?]}]]
  (let [ctx (config/make-ctx)
        me  (api/get-me ctx)]
    (if json?
      (output/print-json me)
      (do
        (output/print-info (str "Username:     " (:username me)))
        (output/print-info (str "Display Name: " (or (:display-name me) "-")))
        (output/print-info (str "Email:        " (or (:email me) "-")))
        (output/print-info (str "User ID:      " (:id me)))
        (when (:status-text me)
          (output/print-info (str "Status:       " (:status-text me))))))
    0))
