(in-package #:lispsearch)

;;;; Search dispatch. Presentation (format-row / format-show / --json) lands with
;;;; the CLI in M4; M2 just needs the query path.

(defun name-pattern (query)
  "Turn a bare QUERY into a GLOB substring pattern. If QUERY already contains
glob metacharacters (* ? [), use it verbatim."
  (if (or (find #\* query) (find #\? query) (find #\[ query))
      query
      (format nil "*~A*" query)))

(defun run-search (db query &key (limit 20) names-only)
  "Search the index. With NAMES-ONLY, do a fast GLOB name match; otherwise a
BM25-ranked full-text search. Returns a list of rows."
  (if names-only
      (search-name db (name-pattern query) :limit limit)
      (search-fts db query :limit limit)))

;;; --- presentation -----------------------------------------------------------

(defun %truncate (s n)
  (let ((s (or s "")))
    (if (> (length s) n)
        (concatenate 'string (subseq s 0 (1- n)) "…")
        s)))

(defun format-row (row &key names-only)
  "One result row as text. Names-only prints just the name; otherwise an aligned
name/version/description line plus an `ocicl install` hint."
  (let ((name (first row)) (latest (or (second row) "")) (desc (or (third row) "")))
    (if names-only
        name
        (format nil "~28A ~18A ~A~%~30T$ ocicl install ~A"
                name latest (%truncate desc 56) name))))

(defun format-show (plist)
  "Full single-record view as text."
  (with-output-to-string (s)
    (format s "~A~@[  (~A)~]~%" (getf plist :system-name) (getf plist :latest))
    (when (getf plist :description) (format s "  ~A~%" (getf plist :description)))
    (format s "  license:  ~A~%" (or (getf plist :license) "-"))
    (format s "  author:   ~A~%" (or (getf plist :author) "-"))
    (format s "  registry: ~A~%" (getf plist :registry-name))
    (format s "  deps:     ~A~%" (or (getf plist :deps) "[]"))
    (format s "  enriched: ~:[no~;yes~]~%" (eql 1 (getf plist :enriched)))
    (format s "  install:  ocicl install ~A~%" (getf plist :system-name))))

(defun %parse-deps (deps-json)
  "Parse a stored deps JSON string back to a vector; empty vector on nil/error."
  (or (ignore-errors (com.inuoe.jzon:parse (or deps-json "[]"))) #()))

(defun %row->ht (row &key names-only)
  (let ((ht (make-hash-table :test 'equal)))
    (setf (gethash "name" ht) (first row))
    (unless names-only
      (setf (gethash "latest" ht) (second row))
      (setf (gethash "description" ht) (third row))
      (setf (gethash "deps" ht) (%parse-deps (fourth row))))
    ht))

(defun rows->json (rows &key names-only)
  "Serialize search rows as a pretty JSON array of objects."
  (com.inuoe.jzon:stringify
   (map 'vector (lambda (r) (%row->ht r :names-only names-only)) rows)
   :pretty t))

(defun system->json (plist)
  "Serialize a full record plist as a JSON object (deps as an array)."
  (let ((ht (make-hash-table :test 'equal)))
    (loop for (k v) on plist by #'cddr
          do (setf (gethash (string-downcase (symbol-name k)) ht)
                   (if (eq k :deps) (%parse-deps v) v)))
    (com.inuoe.jzon:stringify ht :pretty t)))
