#!/usr/bin/env guile
;; PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
;; Copyright 2025 TimeHexOn & foxhop & russell@unturf
;; https://www.permacomputer.com

!#

;;; UncloseAI GNU Guile Library - OpenAI-compatible API client with streaming support
;;; Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
;;;
;;; Library Functions:
;;;   (uncloseai-init)                           - Initialize client with model discovery
;;;   (uncloseai-list-models)                    - List discovered models
;;;   (uncloseai-chat messages model-id)         - Non-streaming chat completion
;;;   (uncloseai-chat-stream messages model-id)  - Streaming chat completion
;;;   (uncloseai-tts text voice model)           - Text-to-speech generation

(use-modules (web client)
             (web response)
             (ice-9 textual-ports)
             (ice-9 rdelim)
             (ice-9 binary-ports)
             (rnrs bytevectors)
             (json)
             (srfi srfi-1)
             (srfi srfi-43))

;;; Global client state
(define *model-ids* '())
(define *model-endpoints* '())
(define *model-max-tokens* '())
(define *tts-endpoints* '())

;;; Initialize client and discover models from environment variables
(define (uncloseai-init)
  (set! *model-ids* '())
  (set! *model-endpoints* '())
  (set! *model-max-tokens* '())
  (set! *tts-endpoints* '())

  ;; Discover chat/code models from MODEL_ENDPOINT_N (N = 1 to 9999)
  (let loop ((i 1))
    (when (<= i 9999)
      (let ((endpoint (getenv (string-append "MODEL_ENDPOINT_" (number->string i)))))
        (if endpoint
            (begin
              ;; Fetch models from endpoint
              (catch #t
                (lambda ()
                  (call-with-values
                      (lambda ()
                        (http-get (string-append endpoint "/models")))
                    (lambda (response body)
                      (when (= (response-code response) 200)
                        (let* ((body-string (if (bytevector? body)
                                                (utf8->string body)
                                                body))
                               (json-response (json-string->scm body-string))
                               (models-data (assoc-ref json-response "data")))
                          (when models-data
                            ;; Process each model in the response (handle both vector and list)
                            (let ((models-list (if (vector? models-data)
                                                   (vector->list models-data)
                                                   models-data)))
                              (for-each
                               (lambda (model)
                                 (let ((model-id (assoc-ref model "id")))
                                   ;; Skip modelperm entries
                                   (when (and model-id
                                             (not (string-prefix? "modelperm-" model-id)))
                                     ;; Extract max_model_len if available (vLLM)
                                     (let ((max-tokens (or (assoc-ref model "max_model_len") 8192)))
                                       (set! *model-ids* (append *model-ids* (list model-id)))
                                       (set! *model-endpoints* (append *model-endpoints* (list endpoint)))
                                       (set! *model-max-tokens* (append *model-max-tokens* (list max-tokens)))))))
                               models-list))))))))
                (lambda (key . args)
                  ;; Silently skip endpoints that fail
                  #f))
              (loop (+ i 1)))
            #f))))

  ;; Discover TTS endpoints from TTS_ENDPOINT_N (N = 1 to 9999)
  (let loop ((i 1))
    (when (<= i 9999)
      (let ((endpoint (getenv (string-append "TTS_ENDPOINT_" (number->string i)))))
        (if endpoint
            (begin
              (set! *tts-endpoints* (append *tts-endpoints* (list endpoint)))
              (loop (+ i 1)))
            #f))))

  (length *model-ids*))

;;; List all discovered models
(define (uncloseai-list-models)
  (for-each
   (lambda (model-id max-tokens)
     (format #t "  - ~a (max_tokens: ~a)\n" model-id max-tokens))
   *model-ids*
   *model-max-tokens*))

;;; Get model index by ID or return 0 for first model
(define (uncloseai-get-model-idx model-id)
  (if (or (not model-id) (string=? model-id ""))
      0  ;; Return first model index
      (let loop ((i 0))
        (cond
         ((>= i (length *model-ids*)) -1)  ;; Not found
         ((string=? (list-ref *model-ids* i) model-id) i)
         (else (loop (+ i 1)))))))

;;; Non-streaming chat completion
;;; Usage: (uncloseai-chat messages model-id max-tokens temperature)
;;; messages: vector of message objects like #(((role . "user") (content . "...")))
;;; Returns: content string
(define* (uncloseai-chat messages #:optional (model-id "") (max-tokens 100) (temperature 0.7))
  (let ((model-idx (uncloseai-get-model-idx model-id)))
    (if (= model-idx -1)
        "ERROR: Model not found"
        (let ((endpoint (list-ref *model-endpoints* model-idx))
              (model (list-ref *model-ids* model-idx)))
          (catch #t
            (lambda ()
              (let ((payload (scm->json-string
                             `((model . ,model)
                               (messages . ,messages)
                               (max_tokens . ,max-tokens)
                               (temperature . ,temperature)
                               (stream . #f)))))
                (call-with-values
                    (lambda ()
                      (http-post (string-append endpoint "/chat/completions")
                                 #:body payload
                                 #:headers '((Content-Type . "application/json"))))
                  (lambda (response body)
                    (if (= (response-code response) 200)
                        (let* ((json-response (json-string->scm (utf8->string body)))
                               (choices (assoc-ref json-response "choices"))
                               (message (assoc-ref (vector-ref choices 0) "message"))
                               (content (assoc-ref message "content")))
                          content)
                        "ERROR: Request failed")))))
            (lambda (key . args)
              (format #f "ERROR: ~a" key)))))))

;;; Streaming chat completion with SSE parsing
;;; Usage: (uncloseai-chat-stream messages model-id max-tokens temperature)
;;; messages: vector of message objects
;;; Prints content chunks as they arrive
(define* (uncloseai-chat-stream messages #:optional (model-id "") (max-tokens 500) (temperature 0.7))
  (let ((model-idx (uncloseai-get-model-idx model-id)))
    (if (= model-idx -1)
        (display "ERROR: Model not found\n")
        (let ((endpoint (list-ref *model-endpoints* model-idx))
              (model (list-ref *model-ids* model-idx)))
          (catch #t
            (lambda ()
              (let ((payload (scm->json-string
                             `((model . ,model)
                               (messages . ,messages)
                               (max_tokens . ,max-tokens)
                               (temperature . ,temperature)
                               (stream . #t)))))
                (call-with-values
                    (lambda ()
                      (http-post (string-append endpoint "/chat/completions")
                                 #:body payload
                                 #:headers '((Content-Type . "application/json"))
                                 #:streaming? #t))
                  (lambda (response port)
                    ;; Read and process SSE stream line by line
                    (let loop ((line (read-line port)))
                      (unless (eof-object? line)
                        (when (string-prefix? "data: " line)
                          (let ((json-data (substring line 6)))
                            (unless (string=? json-data "[DONE]")
                              (catch #t
                                (lambda ()
                                  (let* ((parsed (json-string->scm json-data))
                                         (choices (assoc-ref parsed "choices")))
                                    (when (and choices (> (vector-length choices) 0))
                                      (let* ((delta (assoc-ref (vector-ref choices 0) "delta"))
                                             (content (assoc-ref delta "content")))
                                        (when content
                                          (display content)
                                          (force-output))))))
                                (lambda (key . args) #f)))))
                        (loop (read-line port))))))))
            (lambda (key . args)
              (format #t "\nERROR: ~a\n" key)))))))

;;; Text-to-speech generation
;;; Usage: (uncloseai-tts text voice model output-file)
;;; Returns: file size in bytes (0 on error)
(define* (uncloseai-tts text #:optional (voice "alloy") (model "tts-1") (output-file "/tmp/speech.mp3"))
  (if (null? *tts-endpoints*)
      (begin
        (display "ERROR: No TTS endpoints available\n")
        0)
      (let ((endpoint (car *tts-endpoints*)))
        (catch #t
          (lambda ()
            (let ((payload (scm->json-string
                           `((model . ,model)
                             (voice . ,voice)
                             (input . ,text)))))
              (call-with-values
                  (lambda ()
                    (http-post (string-append endpoint "/audio/speech")
                               #:body payload
                               #:headers '((Content-Type . "application/json"))))
                (lambda (response body)
                  (if (= (response-code response) 200)
                      (begin
                        ;; Write binary response to file
                        (call-with-output-file output-file
                          (lambda (port)
                            (put-bytevector port body)))
                        (stat:size (stat output-file)))
                      0)))))
          (lambda (key . args)
            0)))))

;;; Demo usage when run as script
(when (batch-mode?)
  (display "=== UncloseAI GNU Guile Client (with Streaming) ===\n\n")

  ;; Initialize client with model discovery
  (let ((model-count (uncloseai-init)))
    (if (= model-count 0)
        (begin
          (display "ERROR: No models discovered. Set environment variables:\n")
          (display "  MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.\n")
          (exit 1))
        (begin
          (format #t "Discovered ~a model(s)\n" model-count)
          (uncloseai-list-models)
          (display "\n")

          ;; Non-streaming chat example
          (display "=== Non-Streaming Chat ===\n")
          (let* ((messages #(((role . "system")
                             (content . "You are a helpful AI assistant."))
                            ((role . "user")
                             (content . "Explain quantum computing in one sentence."))))
                 (response (uncloseai-chat messages)))
            (format #t "Model: ~a\n" (car *model-ids*))
            (format #t "Response: ~a\n\n" response))

          ;; Streaming chat example
          (display "=== Streaming Chat ===\n")
          (let* ((model-id (if (>= model-count 2)
                              (list-ref *model-ids* 1)
                              (car *model-ids*)))
                 (messages #(((role . "system")
                             (content . "You are a coding assistant."))
                            ((role . "user")
                             (content . "Write a Scheme function to calculate factorial.")))))
            (format #t "Model: ~a\n" model-id)
            (display "Response: ")
            (uncloseai-chat-stream messages model-id 200)
            (display "\n\n"))

          ;; TTS example
          (when (not (null? *tts-endpoints*))
            (display "=== TTS Speech Generation ===\n")
            (let* ((text "Hello from UncloseAI GNU Guile client! This demonstrates text to speech with streaming support.")
                   (output-file "/tmp/speech.mp3")
                   (file-size (uncloseai-tts text "alloy" "tts-1" output-file)))
              (if (> file-size 0)
                  (format #t "[OK] Speech file created: ~a (~a bytes)\n\n" output-file file-size)
                  (display "[ERROR] TTS generation failed\n\n"))))

          (display "=== Examples Complete ===\n")))))
