#!/usr/bin/env bb
;; Connection-profile selection — Babashka/Clojure reference implementation.
;;
;; Mirror of select_profile.py for the llm-proxy.bb side. The two MUST stay
;; semantically identical: same eligibility rule, same ordering, same offline
;; reasons. Pure functions over plain maps (the parsed profiles JSON) — no
;; state, no I/O except the optional CLI entrypoint at the bottom.
;;
;; Core rule: snappable iff TESTED, not merely AVAILABLE.
;;   eligible? <=> (and (not legacy) available tested)
;; Ordering: [priority asc, latency-ms asc (nil last), id asc].

(ns select-profile
  (:require [cheshire.core :as json]
            [clojure.java.io :as io]))

;; --- offline reasons (stable keywords; map to display strings) -------------

(def reason->display
  {:ready       "Ready"
   :unreachable "Unreachable"
   :unverified  "Reachable, unverified"
   :legacy      "Legacy — not wired"
   :no-profiles "No profiles configured"})

;; --- state coercion --------------------------------------------------------
;; Defensive: a missing/partial state degrades to offline, never throws.

(defn- coerce-state [state]
  (let [s (or state {})]
    {:available  (boolean (get s "available" false))
     :tested     (boolean (get s "tested" false))
     :last-ok    (get s "last_ok")
     :latency-ms (when-let [l (get s "latency_ms")]
                   (when (number? l) (long l)))}))

(defn normalize
  "Parse one schema-shaped profile map (string keys) into a normalized map."
  [p]
  (when-not (and (get p "id") (get p "label") (get p "kind"))
    (throw (ex-info "profile missing required field" {:profile p})))
  {:id       (get p "id")
   :label    (get p "label")
   :kind     (get p "kind")
   :target   (get p "target" "")
   :route    (get p "route" "")
   :models   (get p "models" "auto")
   :tags     (vec (get p "tags" []))
   :priority (long (get p "priority" 100))
   :summary  (get p "summary" "")
   :note     (get p "note" "")
   :state    (coerce-state (get p "state"))})

(defn load-profiles
  "Parse a schema document map into a vector of normalized profiles."
  [doc]
  (mapv normalize (get doc "profiles" [])))

;; --- predicates ------------------------------------------------------------

(defn eligible?
  "Snappable iff reachable AND a real inference round-trip succeeded."
  [{:keys [kind state]}]
  (and (not= kind "legacy")
       (:available state)
       (:tested state)))

(defn offline-reason
  "Machine-readable explanation; :ready when eligible."
  [{:keys [kind state]}]
  (cond
    (= kind "legacy")        :legacy
    (not (:available state)) :unreachable
    (not (:tested state))    :unverified
    :else                    :ready))

;; --- ordering --------------------------------------------------------------
;; nil latency sorts last via a [group value] vector key, matching the Python
;; (1 0) sentinel: measured latencies use group 0, nil uses group 1.

(defn- sort-key [{:keys [priority state id]}]
  (let [lat (:latency-ms state)
        latency-key (if (nil? lat) [1 0] [0 lat])]
    [priority latency-key id]))

(defn rank-eligible
  "Eligible profiles in snap order (best first)."
  [profiles]
  (->> profiles (filter eligible?) (sort-by sort-key)))

;; --- selection -------------------------------------------------------------

(defn select-best
  "Snap to the best ready path, else surface an honest fallback.
   Returns {:profile <map-or-nil> :ready bool :reason <kw>}."
  [profiles]
  (let [profiles (vec profiles)]
    (cond
      (empty? profiles)
      {:profile nil :ready false :reason :no-profiles}

      :else
      (if-let [best (first (rank-eligible profiles))]
        {:profile best :ready true :reason :ready}
        (let [fallback (first (sort-by sort-key profiles))]
          {:profile fallback :ready false :reason (offline-reason fallback)})))))

(defn selection-display [{:keys [profile reason]}]
  (if (nil? profile)
    (reason->display reason)
    (str (:label profile) " — " (reason->display reason))))

;; --- CLI entrypoint --------------------------------------------------------

(defn -main [& args]
  (let [path  (or (first args) "profiles.example.json")
        doc   (json/parse-string (slurp (io/file path)))
        profs (load-profiles doc)]
    (println "rank (eligible, best first):")
    (doseq [p (rank-eligible profs)]
      (println (format "  %4d  %5sms  %s"
                        (:priority p)
                        (str (get-in p [:state :latency-ms]))
                        (:id p))))
    (let [sel (select-best profs)]
      (println)
      (println (str "selected: " (selection-display sel) " (ready=" (:ready sel) ")")))
    (println "\nall profiles with reasons:")
    (doseq [p profs]
      (println (format "  %-22s %s" (:id p) (name (offline-reason p)))))))

(when (= *file* (System/getProperty "babashka.file"))
  (apply -main *command-line-args*))
