;; SPDX-FileCopyrightText: 2026 Andrew Oberstar <andrew@ajoberstar.org>
;; SPDX-License-Identifier: AGPL-3.0-only

(in-package #:org.ajoberstar.quixote.openlibrary)

(defparameter headers `(("User-Agent" . ,(format nil "Quixote/~A (andrew@ajoberstar.org)"
                                                 (asdf:component-version (asdf:find-system "org.ajoberstar.quixote"))))))

(defvar *author-cache* (make-hash-table :test #'equalp))
(defvar *work-cache* (make-hash-table :test #'equalp))
(defvar *book-cache* (make-hash-table :test #'equalp))
(defvar *isbn-cache* (make-hash-table :test #'equalp))

(defun clear-caches ()
  (setf *author-cache* (make-hash-table :test #'equalp))
  (setf *work-cache* (make-hash-table :test #'equalp))
  (setf *book-cache* (make-hash-table :test #'equalp))
  (setf *isbn-cache* (make-hash-table :test #'equalp))
  nil)

(defun olid (key)
  (when key
    (third (uiop:split-string key :separator '(#\/)))))

(defun %author (olid)
  (when olid
    (handler-bind ((dex:http-request-not-found #'dex:ignore-and-continue))
      (let* ((json (dex:get (format nil "https://openlibrary.org/authors/~A.json" olid)
                            :headers headers))
             (data (jzon:parse json))
             (remote-ids (gethash "remote_ids" data)))
        (model:make-author :name (gethash "name" data)
                           :sort-name nil
                           :birth (dates:parse-lax-date (gethash "birth_date" data))
                           :death (dates:parse-lax-date (gethash "death_date" data))
                           :bookbrainz-id (when remote-ids (gethash "bookbrainz" remote-ids))
                           :openlibrary-id olid
                           :wikidata-id (when remote-ids (gethash "wikidata" remote-ids)))))))

(defun author (olid)
  (when olid
    (let ((cached (gethash olid *author-cache*)))
      (if cached
          cached
          (progn
            (setf (gethash olid *author-cache*)
                  (%author olid)))))))

(defun %work (olid)
  (when olid
    (handler-bind ((dex:http-request-not-found #'dex:ignore-and-continue))
      (let* ((json (dex:get (format nil "https://openlibrary.org/works/~A.json" olid)
                            :headers headers))
             (data (jzon:parse json)))
        (list (cons "part" (model:make-part :title (gethash "title" data)
                                            :sort-title nil
                                            :segment "body"
                                            :page-start nil
                                            :page-end nil
                                            :bookbrainz-id nil
                                            :openlibrary-id olid
                                            :wikidata-id nil))
              (cons "date" (dates:parse-lax-date (gethash "first_publish_date" data)))
              (cons "author" (loop for author across (gethash "authors" data)
                                   for key = (gethash "key" (gethash "author" author))
                                   for olid = (olid key)
                                   return (author olid))))))))

(defun work (olid)
  (when olid
    (let ((cached (gethash olid *work-cache*)))
      (if cached
          cached
          (progn
            (setf (gethash olid *work-cache*)
                  (%work olid)))))))

(defun %book (olid)
  (when olid
    (handler-bind ((dex:http-request-not-found #'dex:ignore-and-continue))
      (let* ((json (dex:get (format nil "https://openlibrary.org/books/~A.json" olid)
                            :headers headers))
             (data (jzon:parse json))
             (work (loop for work across (gethash "works" data)
                         for key = (gethash "key" work)
                         for olid = (olid key)
                         return (work olid))))
        (model:make-edition :title (gethash "title" data)
                            :sort-title nil
                            :disambiguation nil
                            :author (cdr (assoc "author" work :test #'equalp))
                            :translator nil
                            :page-start 1
                            :page-end (or (gethash "number_of_pages" data)
                                          (let ((pagination (gethash "pagination" data)))
                                            (and pagination (parse-integer pagination :junk-allowed t))))
                            :type nil
                            :year (or (cdr (assoc "date" work :test #'equalp))
                                      (dates:parse-lax-date (gethash "publish_date" data)))
                            ;; for now, assume everything will have ISBN13
                            :isbn (let ((isbn (gethash "isbn_13" data)))
                                    (when isbn (elt isbn 0)))
                            :bookbrainz-id nil
                            :openlibrary-id olid
                            :wikidata-id nil)))))

(defun book (olid)
  (when olid
    (let ((cached (gethash olid *book-cache*)))
      (if cached
          cached
          (progn
            (setf (gethash olid *book-cache*)
                  (%book olid)))))))

(defun %book-search (isbn)
  (when isbn
    (handler-bind ((dex:http-request-not-found #'dex:ignore-and-continue))
      (let* ((json (dex:get (format nil "https://openlibrary.org/api/books.json?bibkeys=ISBN:~A" isbn)
                            :headers headers))
             (data (jzon:parse json))
             (result (gethash (format nil "ISBN:~A" isbn) data)))
        (when result
          (multiple-value-bind (full groups)
              (ppcre:scan-to-strings "/books/([^/]+)/" (gethash "info_url" result))
            (declare (ignore full))
            (when groups
              (book (elt groups 0)))))))))

(defun book-search (isbn)
  (when isbn
    (let ((cached (gethash isbn *isbn-cache*)))
      (if cached
          cached
          (progn
            (setf (gethash isbn *isbn-cache*)
                  (%book-search isbn)))))))
