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

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

;;; Base

(defparameter *static-prefix* "/static/")

(defvar *app* (make-instance 'ningle:app))

(defvar *handler* nil)

(defun stop ()
  (when *handler*
    (format t "Stopping app~%")
    (clack:stop *handler*)))

(defun start (&key (address "127.0.0.1") (port 5000) connect-args static-dir (output *error-output*))
  (when *handler*
    (stop))
  (format t "Starting app~%")
  (setq *handler*
        (clack:clackup
         (lack:builder
          :accesslog
          (:backtrace :output output
                      :result-on-error `(500 (:content-type "text/plain") ("Internal Server Error")))
          (:static :path (lambda (path)
                           (if (ppcre:scan "^(?:/images/|/css/|/js/|/robots\\.txt$|/favicon.ico$)" path)
                               path))
                   :root static-dir)
          ;; wrap with db connection
          (lambda (app)
            (lambda (env)
              (db:with-connection (connect-args)
                (funcall app env))))
          *app*)
         :server :hunchentoot
         :address address
         :port port
         :debug nil
         :use-default-middlewares nil)))

(defun dev ()
  (let* ((sysdir (asdf:system-source-directory "org.ajoberstar.quixote"))
         (connect-args (list :sqlite3 :database-name (merge-pathnames "quixote.db" sysdir))))
    (db:with-connection (connect-args)
      (db:sql-migrate))
    (start :connect-args connect-args
           :static-dir (merge-pathnames "static/" sysdir))))

;;; Templates

(defmacro with-base-template ((&key title charts-p) &body body)
  `(let ((*html-style* :tree)
         (*always-quote* t))
     (with-html-string
       (:doctype)
       (:html
        (:head
         (:meta :name "viewport" :content "width=device-width, initial-scale=1.0")
         (:link :rel "stylesheet" :href "/css/main.css")
         (when ,charts-p
           (:script :src "/js/vendor/chart.js")

           (:script (:raw (ps:ps*
                           `(progn
                              (defun css-var (name)
                                (let* ((style (get-computed-style (ps:chain document document-element)))
                                       (value (ps:chain style (get-property-value name))))
                                  (ps:chain value (trim)))))))))
         (:title ,title))
        (:body
         (:header
          (:a :href "/" "Quixote")
          (:nav
           (:ul
            (:li (:a :href "/books" "Books"))
            (:li (:a :href "/readthroughs" "Readthroughs"))
            (:li (:a :href "/readings" "Readings")))))
         (:main
          ,@body))))))

(defun delete-template (page)
  (with-html
    (:section :class "form"
              (:h3 "Delete")
              (:form :method :post :action page
                     (:label :for "confirm1" "Are you sure?")
                     (:input :type "checkbox" :name "confirm1")
                     (:label :for "confirm2" "Are you really sure?")
                     (:input :type "checkbox" :name "confirm2")
                     (:button :type "submit" "Delete")))))

(defun readings-template (readings)
  (with-html
    (:table :class "readings"
            (:thead
             (:tr
              (:th "Date")
              (:th "Book")
              (:th "Start")
              (:th "End")
              (:th "Pages")))
            (:tbody
             (loop for reading in readings
                   do (:tr
                       (:td (:a :href (format nil "/readings/~d" (model:reading-id reading))
                                (write-date (model:reading-at reading))))
                       (:td (model:book-title reading))
                       (:td (model:reading-page-start reading))
                       (:td (model:reading-page-end reading))
                       (:td (model:reading-pages reading))))))))

(defun readthrough-template (readthrough &key (mode :standalone))
  (let* ((readings-p (eql mode :full))
         (book-p (member mode (list :standalone :full)))
         (book (model:readthrough-book readthrough))
         (readings (db:select-readthrough-readings readthrough))
         (progress (if (eq 0 (length readings))
                       0
                       (apply #'max (mapcar #'model:reading-page-end readings))))
         (goal (model:book-pages (model:readthrough-book readthrough)))
         (avg (if (eq 0 (length readings))
                  0
                  (round (/ (reduce #'+ (mapcar #'model:reading-pages readings))
                            (length readings))))))
    (with-html
      (:section :class (if book-p "readthrough covered" "readthrough")
                (when book-p
                  (:section :class "cover"
                            (:img :src (format nil "/books/~d/cover" (model:book-id book)))))
                (:section :class "content"
                          (case mode
                            (:full (:h2 "Information"))
                            (:standalone
                             (:h3
                              (:a :href (format nil "/readthroughs/~d" (model:entity-id readthrough))
                                  (model:book-title readthrough))))
                            (:simple
                             (:h3
                              (:a :href (format nil "/readthroughs/~d" (model:entity-id readthrough))
                                  (format nil "~A - ~A"
                                          (write-date (model:readthrough-started-at readthrough))
                                          (write-date (model:readthrough-completed-at readthrough)))))))

                          (unless (model:readthrough-completed-at readthrough)
                            (:progress :value progress :max goal))

                          (:section :class "details"
                                    (:dl
                                     (:dt "Started")
                                     (:dd (write-date (model:readthrough-started-at readthrough)))

                                     (:dt "Completed")
                                     (:dd (write-date (model:readthrough-completed-at readthrough)))

                                     (:dt "Progress")
                                     (:dd (format nil "~:d p. / ~:d p." progress goal))

                                     (:dt "Percent")
                                     (:dd (format nil "~d%"
                                                  (if (< 0 goal)
                                                      (round (* 100 (/ progress goal)))
                                                      0)))

                                     (:dt "Remaining")
                                     (:dd (if (and (null (model:readthrough-completed-at readthrough))
                                                   (< 0 goal)
                                                   (< 0 avg))
                                              (format nil " ~d days"
                                                      (round (/ (- goal progress) avg)))
                                              (:em "complete")))

                                     (:dt "Pace")
                                     (:dd (format nil "~:d p. / day" avg))))))

      (when readings-p
        (:section :class "readings"
                  (:h2 "Readings")
                  (readings-template readings)))

      (when (and readings-p (null (model:readthrough-completed-at readthrough)))
        (let ((current-reading (first readings)))
          (if (dates:date= (dates:today) (model:reading-at current-reading))
              (:section :class "form"
                        (:h3 "Update Today's Reading")
                        (:form :method "post" :action (format nil "/readings/~d" (model:entity-id current-reading))
                               (:input :type "hidden" :name "at" :value (write-date (model:reading-at current-reading)))
                               (:label :for "page_start" "Start Page")
                               (:input :type "number" :name "page_start" :value (model:reading-page-start current-reading))
                               (:label :for "page_end" "End Page")
                               (:input :type "number" :name "page_end" :value (model:reading-page-end current-reading))
                               (:button :type "submit" "Submit")))
              (:section :class "form"
                        (:h3 "Add Reading")
                        (:form :method :post :action "/readings"
                               (:input :type "hidden" :name "readthrough_id" :value (model:entity-id readthrough))
                               (:label :for "page_start" "Start Page")
                               (:input :type "number" :name "page_start" :value progress)
                               (:label :for "page_end" "End Page")
                               (:input :type "number" :name "page_end")
                               (:button :type "submit" "Submit")))))))))

;;; Helpers

(defun getparam (params key)
  (let ((value (cdr (assoc key params :test #'string-equal))))
    (when value
      (string-trim '(#\Space #\Tab #\Newline #\Linefeed #\Return) value))))

(defun write-date (date)
  (if date (dates:write-iso-date nil date) "..."))

(defun safe-parse-integer (string)
  (declare (type string string))
  (handler-case (parse-integer string)
    (error (c)
      (declare (ignore c))
      nil)))

;;; Routes

(setf (ningle:route *app* "/" :method :get)
      #'(lambda (params)
          (declare (ignore params))
          (with-base-template (:title "Quixote - Home" :charts-p t)
            (:h1 "Quixote")

            (:h2 "Active Readthroughs")
            (loop for readthrough in (db:select-active-readthroughs)
                  do (readthrough-template readthrough :mode :standalone))

            (:h2 "Charts")
            (:section :class "chart"
                      (:h3 "Pages per Week")
                      (:canvas :id "week_chart"))
            (:section :class "chart"
                      (:h3 "Pages per Quarter")
                      (:canvas :id "quarter_chart"))
            (:section :class "chart"
                      (:h3 "Pages per Year")
                      (:canvas :id "year_chart"))

            (let* ((readings (db:select-readings))
                   (year-pages (stats:year-pages readings))
                   (quarter-start (let ((current-quarter (dates:date-start-of-quarter (dates:today))))
                                    (dates:make-date (- (dates:date-year current-quarter) 3)
                                                     (dates:date-month current-quarter)
                                                     (dates:date-day current-quarter))))
                   (quarter-pages (stats:quarter-pages quarter-start
                                                       (remove-if-not (lambda (reading)
                                                                        (dates:date< quarter-start (model:reading-at reading)))
                                                                      readings)))
                   (week-start (let ((current-week (dates:date-prev (dates:today) :monday)))
                                 (dates:date- current-week (* 13 7))))
                   (week-pages (stats:week-pages week-start
                                                 (remove-if-not (lambda (reading)
                                                                  (dates:date< week-start (model:reading-at reading)))
                                                                readings))))
              (:script
               (:raw (ps:ps*
                      `(progn
                         (let ((ctx (ps:chain document (get-element-by-id "year_chart"))))
                           (ps:new (-Chart ctx (ps:create
                                                :type "bar"
                                                :data (ps:create
                                                       :labels (list ,@(getf year-pages :labels))
                                                       :datasets (list
                                                                  (ps:create :label "Pages"
                                                                             :data (list ,@(getf year-pages :dataset))
                                                                             "backgroundColor" (css-var "--bg-graph-green-1"))))
                                                :options (ps:create
                                                          :animations ps:f)))))
                         (let ((ctx (ps:chain document (get-element-by-id "quarter_chart"))))
                           (ps:new (-Chart ctx (ps:create
                                                :type "bar"
                                                :data (ps:create
                                                       :labels (list ,@(getf quarter-pages :labels))
                                                       :datasets (list
                                                                  (ps:create :label "Pages"
                                                                             :data (list ,@(getf quarter-pages :dataset))
                                                                             "backgroundColor" (css-var "--bg-graph-red-1"))))
                                                :options (ps:create
                                                          :animations ps:f)))))
                         (let ((ctx (ps:chain document (get-element-by-id "week_chart"))))
                           (ps:new (-Chart ctx (ps:create
                                                :type "bar"
                                                :data (ps:create
                                                       :labels (list ,@(getf week-pages :labels))
                                                       :datasets (list
                                                                  (ps:create :label "Pages"
                                                                             :data (list ,@(getf week-pages :dataset))
                                                                             "backgroundColor" (css-var "--bg-graph-cyan-1"))))
                                                :options (ps:create
                                                          :animations ps:f)))))))))))))

(setf (ningle:route *app* "/books" :method :get)
      #'(lambda (params)
          (declare (ignore params))
          (with-base-template (:title "Quixote - Books")
            (:h1 "Books")
            (:section :class "books"
                      (loop for book in (db:select-books)
                            do (:section :class "book"
                                         (:img :src (format nil "/books/~d/cover" (model:book-id book)))
                                         (:p
                                          (:a :href (format nil "/books/~d" (model:entity-id book))
                                              (model:book-title book))))))
            (:section
             (:h3 "New Book")
             (:form :method "post" :action ""
                    (:label :for "title" "Title")
                    (:input :type "text" :name "title")
                    (:label :for "type" "Type")
                    (:input :type "text" :name "type")
                    (:label :for "pages" "Pages")
                    (:input :type "number" :name "pages" :placeholder "Pages")
                    (:button :type "submit" "Submit"))))))

(setf (ningle:route *app* "/books" :method :post)
      #'(lambda (params)
          (let* ((title (getparam params "title"))
                 (type (getparam params "type"))
                 (pages (safe-parse-integer (getparam params "pages")))
                 (book (db:insert-book (model:make-book title nil nil type pages nil))))
            `(303 (:location ,(format nil "/books/~d" (model:entity-id book))) ()))))

(setf (ningle:route *app* "/books/:id" :method :get)
      #'(lambda (params)
          (let* ((id (parse-integer (cdr (assoc :id params))))
                 (book (db:select-book id)))
            (with-base-template (:title (format nil "Quixote - ~A" (model:book-title book)))
              (:h1 (model:book-title book))
              (:section :class "book covered"
                        (:section :class "cover"
                                  (:img :src (format nil "/books/~d/cover" (model:book-id book))))
                        (:section :class "content"
                                  (:h2 "Information")
                                  (:dl
                                   (:dt "Author")
                                   (:dd (model:book-author book))

                                   (:dt "Year")
                                   (:dd (model:book-year book))

                                   (:dt "Type")
                                   (:dd (model:book-type book))

                                   (:dt "Pages")
                                   (:dd (model:book-pages book))

                                   (:dt "ISBN")
                                   (:dd (model:book-isbn book)))))

              (:h2 "Readthroughs")
              (loop for readthrough in (db:select-book-readthroughs book)
                    do (readthrough-template readthrough :mode :simple))

              (:h2 "Forms")
              (:section :class "form"
                        (:h3 "Update Book")
                        (:form :method "post" :action ""
                               (:label :for "title" "Title")
                               (:input :type "text" :name "title" :value (model:book-title book))
                               (:label :for "author" "Author")
                               (:input :type "text" :name "author" :value (model:book-author book))
                               (:label :for "year" "Year")
                               (:input :type "number" :name "year" :value (model:book-year book))
                               (:label :for "type" "Type")
                               (:input :type "text" :name "type" :value (model:book-type book))
                               (:label :for "pages" "Pages")
                               (:input :type "number" :name "pages" :value (model:book-pages book))
                               (:label :for "isbn" "ISBN")
                               (:input :type "text" :name "isbn" :value (model:book-isbn book))
                               (:button :type "submit" "Submit")))
              (:section :class "form"
                        (:h3 "Upload Cover")
                        (:form :method "post" :action (format nil "/books/~d/cover" (model:book-id book))
                               :enctype "multipart/form-data"
                               (:label :for "cover" "Cover")
                               (:input :type "file" :name "cover" :accept "image/jpeg")
                               (:button :type "submit" "Submit")))
              (:section :class "form"
                        (:h3 "Remove Cover")
                        (:form :method "post" :action (format nil "/books/~d/cover/delete" (model:book-id book))
                               (:button :type "submit" "Submit")))
              (:section :class "form"
                        (:h3 "Fetch from OpenLibrary")
                        (:form :method "post" :action (format nil "/books/~d/openlibrary" (model:entity-id book))
                               (:label :for "isbn" "ISBN")
                               (:input :type "text" :name "isbn" :value (model:book-isbn book))
                               (:button :type "submit" "Fetch")))
              (:section :class "form"
                        (:h3 "New Readthrough")
                        (:form :method :post :action "/readthroughs"
                               (:input :type "hidden" :name "book_id" :value (model:entity-id book))
                               (:label :for "start_date" "Start Date")
                               (:input :type "date" :name "start_date" :value (dates:write-iso-date nil (dates:today)))
                               (:button :type "submit" "Submit")))
              (delete-template (format nil "/books/~d/delete" (model:entity-id book)))))))

(setf (ningle:route *app* "/books/:id/cover" :method :get)
      #'(lambda (params)
          (let* ((id (parse-integer (cdr (assoc :id params))))
                 (cover (db:select-book-cover id)))
            (if cover
                `(200 (:content-type "image/jpeg")
                      ,cover)
                `(404 (:content-type "text/plain")
                      ("Not Found"))))))

(setf (ningle:route *app* "/books/:id/cover" :method :post)
      #'(lambda (params)
          (let* ((id (parse-integer (cdr (assoc :id params))))
                 (cover (cdr (assoc "cover" params :test #'string-equal)))
                 (cover-data (alexandria:read-stream-content-into-byte-vector (first cover))))
            (db:update-book-cover id (if (< 0 (length cover-data))
                                         cover-data))
            `(303 (:location ,(format nil "/books/~d" id)) ()))))

(setf (ningle:route *app* "/books/:id/cover/delete" :method :post)
      #'(lambda (params)
          (let* ((id (parse-integer (cdr (assoc :id params)))))
            (db:delete-book-cover id)
            `(303 (:location ,(format nil "/books/~d" id)) ()))))

(setf (ningle:route *app* "/books/:id" :method :post)
      #'(lambda (params)
          (let* ((id (parse-integer (cdr (assoc :id params))))
                 (title (getparam params "title"))
                 (author (getparam params "author"))
                 (year (safe-parse-integer (getparam params "year")))
                 (type (getparam params "type"))
                 (pages (safe-parse-integer (getparam params "pages")))
                 (isbn (getparam params "isbn"))
                 (book (db:select-book id)))
            (setf (model:book-title book) title)
            (setf (model:book-author book) author)
            (setf (model:book-year book) year)
            (setf (model:book-type book) type)
            (setf (model:book-pages book) pages)
            (setf (model:book-isbn book) isbn)
            (db:update-book book)

            `(303 (:location ,(format nil "/books/~d" id)) ()))))

(setf (ningle:route *app* "/books/:id/openlibrary" :method :post)
      #'(lambda (params)
          (let* ((id (parse-integer (cdr (assoc :id params))))
                 (isbn (getparam params "isbn"))
                 (book (db:select-book id)))
            (when isbn
              (let ((result (lib:book-search isbn)))
                (if (model:book-title result)
                    (setf (model:book-title book) (model:book-title result)))
                (if (model:book-author result)
                    (setf (model:book-author book) (model:book-author result)))
                (if (model:book-year result)
                    (setf (model:book-year book) (model:book-year result)))
                (if (model:book-type result)
                    (setf (model:book-type book) (model:book-type result)))
                (if (model:book-pages result)
                    (setf (model:book-pages book) (model:book-pages result)))
                (setf (model:book-isbn book) isbn)
                (db:update-book book)

                (let ((cover (lib:cover isbn)))
                  (when cover
                    (db:update-book-cover id cover)))))

            `(303 (:location ,(format nil "/books/~d" id)) ()))))

(setf (ningle:route *app* "/books/:id/delete" :method :post)
      #'(lambda (params)
          (let ((book (db:select-book (parse-integer (getparam params :id))))
                (confirm1 (getparam params "confirm1"))
                (confirm2 (getparam params "confirm2")))
            (if (and confirm1 confirm2)
                (progn
                  (db:delete-book book)
                  '(303 (:location "/books")))
                '(400 (:content-type "text/plain") ("Bad Request"))))))

(setf (ningle:route *app* "/readthroughs" :method :get)
      #'(lambda (params)
          (declare (ignore params))
          (with-base-template (:title "Quixote - Readthroughs")
            (:h1 "Readthroughs")
            (loop for readthrough in (db:select-readthroughs)
                  do (readthrough-template readthrough :mode :standalone)))))

(setf (ningle:route *app* "/readthroughs" :method :post)
      #'(lambda (params)
          (let* ((id (parse-integer (getparam params "book_id")))
                 (book (db:select-book id))
                 (start (dates:parse-iso-date (getparam params "start_date")))
                 (readthrough (db:insert-readthrough (model:make-readthrough book start nil))))
            `(303 (:location ,(format nil "/readthroughs/~d" (model:entity-id readthrough))) ()))))

(setf (ningle:route *app* "/readthroughs/:id" :method :get)
      #'(lambda (params)
          (let* ((readthrough (db:select-readthrough (parse-integer (getparam params :id)))))
            (with-base-template (:title (format nil "Quixote - ~A Readthrough" (model:book-title readthrough)) :charts-p t)
              (:h1 (:a :href (format nil "/books/~d" (model:book-id readthrough))
                       (model:book-title (model:readthrough-book readthrough))))
              (readthrough-template readthrough :mode :full)

              (:section
               (:h2 "Charts")
               (:section :class "chart"
                         (:h3 "Progress")
                         (:canvas :id "progress_chart")))

              (:section
               (:h2 "Forms")
               (:section :class "form"
                         (:h3 "Update Readthrough")
                         (:form :method "post" :action ""
                                (:label :for "completed" "Completed")
                                (:input :type "checkbox" :name "completed" :checked (not (null (model:readthrough-completed-at readthrough))))
                                (:button :type "submit" "Submit"))))
              (delete-template (format nil "/readthroughs/~d/delete" (model:entity-id readthrough)))

              (let* ((readings (db:select-readthrough-readings readthrough))
                     (progress (stats:progress readings)))
                (:script
                 (:raw (ps:ps*
                        `(let ((ctx (ps:chain document (get-element-by-id "progress_chart"))))
                           (ps:new (-Chart ctx (ps:create
                                                :type "line"
                                                :data (ps:create
                                                       :labels (list ,@(getf progress :labels))
                                                       :datasets (list
                                                                  (ps:create :label "Pages"
                                                                             :data (list ,@ (getf progress :dataset))
                                                                             "backgroundColor" (css-var "--bg-graph-cyan-1")
                                                                             "borderColor" (css-var "--bg-graph-cyan-1"))))
                                                :options (ps:create
                                                          :animations ps:f
                                                          :datasets (ps:create
                                                                     :line (ps:create
                                                                            :stepped ps:f
                                                                            "pointStyle" ps:f))
                                                          :scales (ps:create
                                                                   :y (ps:create
                                                                       "suggestedMax" ,(model:book-pages (model:readthrough-book readthrough)))))))))))))))))

(setf (ningle:route *app* "/readthroughs/:id" :method :post)
      #'(lambda (params)
          (let* ((id (parse-integer (cdr (assoc :id params))))
                 (completed-p (getparam params "completed"))
                 (readthrough (db:select-readthrough id))
                 (readings (db:select-readthrough-readings readthrough))
                 (started (reduce (lambda (a b)
                                    (if (dates:date< a b) a b))
                                  (mapcar #'model:reading-at readings)))
                 (completed (reduce (lambda (a b)
                                      (if (dates:date> a b) a b))
                                    (mapcar #'model:reading-at readings))))
            (setf (model:readthrough-started-at readthrough)
                  started)
            (setf (model:readthrough-completed-at readthrough)
                  (if (and completed-p completed)
                      completed))
            (db:update-readthrough readthrough)
            `(303 (:location ,(format nil "/readthroughs/~d" id)) ()))))

(setf (ningle:route *app* "/readthroughs/:id/delete" :method :post)
      #'(lambda (params)
          (let ((readthrough (db:select-readthrough (parse-integer (getparam params :id))))
                (confirm1 (getparam params "confirm1"))
                (confirm2 (getparam params "confirm2")))
            (if (and confirm1 confirm2)
                (progn
                  (db:delete-readthrough readthrough)
                  `(303 (:location ,(format nil "/books/~d" (model:book-id readthrough)))))
                '(400 (:content-type "text/plain") ("Bad Request"))))))

(setf (ningle:route *app* "/readings" :method :get)
      #'(lambda (params)
          (declare (ignore params))
          (let ((readings (db:select-readings)))
            (with-base-template (:title "Quixote - Readings" :charts-p t)
              (:h1 "Readings")
              (:section
               (:h2 "Charts")
               (:section :class "chart"
                         (:h3 "Pace")
                         (:canvas :id "pace_chart")))
              (:section :class "readings"
                        (:h2 "Readings")
                        (readings-template readings))
              (let* ((pace (stats:pace readings)))
                (:script
                 (:raw (ps:ps*
                        `(let ((ctx (ps:chain document (get-element-by-id "pace_chart"))))
                           (ps:new (-Chart ctx (ps:create
                                                :type "line"
                                                :data (ps:create
                                                       :labels (list ,@(getf pace :labels))
                                                       :datasets (list
                                                                  (ps:create :label "Quarter Pace"
                                                                             :data (list ,@ (getf pace :quarter-dataset))
                                                                             "backgroundColor" (css-var "--bg-graph-cyan-1")
                                                                             "borderColor" (css-var "--bg-graph-cyan-1"))
                                                                  (ps:create :label "Year Pace"
                                                                             :data (list ,@ (getf pace :year-dataset))
                                                                             "backgroundColor" (css-var "--bg-graph-green-1")
                                                                             "borderColor" (css-var "--bg-graph-green-1"))
                                                                  (ps:create :label "Month Pace"
                                                                             :data (list ,@ (getf pace :month-dataset))
                                                                             "backgroundColor" (css-var "--bg-inactive")
                                                                             "borderColor" (css-var "--bg-inactive"))

                                                                  ))
                                                :options (ps:create
                                                          :animations ps:f
                                                          :datasets (ps:create
                                                                     :line (ps:create
                                                                            "pointStyle" ps:f)))))))))))))))

(setf (ningle:route *app* "/readings" :method :post)
      #'(lambda (params)
          (let ((readthrough (db:select-readthrough (parse-integer (getparam params "readthrough_id"))))
                (start (safe-parse-integer (getparam params "page_start")))
                (end (safe-parse-integer (getparam params "page_end"))))
            (db:insert-reading (model:make-reading readthrough
                                                   (dates:today)
                                                   start end))
            `(303 (:location ,(format nil "/readthroughs/~d" (model:readthrough-id readthrough))) ()))))

(setf (ningle:route *app* "/readings/:id" :method :get)
      #'(lambda (params)
          (let ((reading (db:select-reading (parse-integer (getparam params :id)))))
            (with-base-template (:title (format nil "Quixote - ~A ~A Reading"
                                                (model:book-title reading)
                                                (dates:write-iso-date nil (model:reading-at reading))))
              (:h1 (:a :href (format nil "/readthroughs/~d" (model:readthrough-id reading))
                       (model:book-title reading)))
              (:section
               (:h2 "Forms")
               (:section :class "form"
                         (:h3 "Update Reading")
                         (:form :method "post" :action ""
                                (:label :for "at" "Date")
                                (:input :type "date" :name "at" :value (write-date (model:reading-at reading)))
                                (:label :for "page_start" "Start Page")
                                (:input :type "number" :name "page_start" :value (model:reading-page-start reading))
                                (:label :for "page_end" "End Page")
                                (:input :type "number" :name "page_end" :value (model:reading-page-end reading))
                                (:button :type "submit" "Submit")))
               (delete-template (format nil "/readings/~d/delete" (model:reading-id reading))))))))

(setf (ningle:route *app* "/readings/:id" :method :post)
      #'(lambda (params)
          (let ((reading (db:select-reading (parse-integer (getparam params :id))))
                (at (dates:parse-iso-date (getparam params "at")))
                (start (safe-parse-integer (getparam params "page_start")))
                (end (safe-parse-integer (getparam params "page_end"))))
            (setf (model:reading-at reading) at)
            (setf (model:reading-page-start reading) start)
            (setf (model:reading-page-end reading) end)
            (db:update-reading reading)
            `(303 (:location ,(format nil "/readthroughs/~d" (model:readthrough-id reading)))))))

(setf (ningle:route *app* "/readings/:id/delete" :method :post)
      #'(lambda (params)
          (let ((reading (db:select-reading (parse-integer (getparam params :id))))
                (confirm1 (getparam params "confirm1"))
                (confirm2 (getparam params "confirm2")))
            (if (and confirm1 confirm2)
                (progn
                  (db:delete-reading reading)
                  `(303 (:location ,(format nil "/readthroughs/~d" (model:readthrough-id reading)))))
                '(400 (:content-type "text/plain") ("Bad Request"))))))
