#|

src/models/crystal-commerce-csv.lisp

Crystal Commerce Search CSV Export / Import functionality.

This code is so old. It dates back to May. I mistakenly assumed we
would get access to Crystal Commerce or TCGPlayer API data.

Instead, I based my initial database design off of the CSV export
data. The CSV export data does not supply the following necessary
fields: Passcode (Card Image), Variants (Condition).

I learned July 15 that this design was insufficient and set out to
replace it with something more robust. Within 2 weeks by the beginning
of August I had conceptualized a new design, and by the second week of
August, had implemented that design. That is currenly YGOPRODECK-2.

There is some munging facility in here that I wrote while munging the
YGOProDeck API data during the v2 rewrite.

TODO This needs to be rewritten to match the new YGOProDeck v3
API. Currently we scan in the Product Name, it could be split out to
match the corresponding db entry.

TODO Some time in October I became aware we have access to Crystal Commerce API now.

https://crystal-service.readme.io/docs/get-started-with-the-admin-api
https://crystal-service.readme.io/reference/get_api-v1-activity-logs

|#

(in-package #:cl-user)

(defpackage #:cl-deck-builder2.models.crystal-commerce.csv
  (:use #:cl
        #:cl-deck-builder2.db
        #:cl-deck-builder2.models.generics
        #:cl-deck-builder2.models.crystal-commerce)
  (:import-from #:cl-deck-builder2.toolkit
                #:grouped)
  (:local-nicknames (#:v #:org.shirakumo.verbose))
  (:export #:*cc-csv-import-fields*
           #:*cc-csv-header-fields*
           #:csv-import-cc))

(in-package #:cl-deck-builder2.models.crystal-commerce.csv)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CSV Import
(defvar *csv* nil
  "The currently loaded CSV data.")

(defvar *card-sets* nil
  "Data loaded from CARD-SET table. We use datafly for this one because
mito conses a lot for CLOS. A PLIST is faster.")

(defvar *cc-csv-import-fields*
  '("Product Name"
    "Category"
    "Total Qty"
    "Wishlists"
    "Buy Price"
    "Sell Price"
    "URL"
    "Barcode"
    "Manufacturer SKU"
    "Amazon ASIN"
    "MSRP"
    "Brand"
    "Weight"
    "Description"
    "Max Qty"
    "Domestic Only"
    "Tax Exempt")
  "Crystal Commerce header fields via CSV import. We transform this into *CC-CSV-HEADER-FIELDS*.

SEE *CC-CSV-HEADER-FIELDS*.")

(defvar *cc-csv-header-fields*
  (mapcar (lambda (s)
            (alexandria:make-keyword
             (string-upcase
              (substitute #\- #\Space s))))
          *cc-csv-import-fields*)
  "Transformed list of Keyword-ified header fields. That is, we take *CC-CSV-IMPORT-FIELDS* and transform the strings into keywords:

    \"Product Name\" => :PRODUCT-NAME.")

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun load-csv (maybe-pathname &optional (force nil))
  "Cache CSV data from MAYBE-PATHNAME which may be a PATHNAME or a STRING containing CSV data."
  (if (or force
          (null *csv*))
      (progn
        (v:info :cc-csv "Loading CSV from ~A~%"
                (typecase maybe-pathname
                  (pathname maybe-pathname)
                  (string 'STRING)))
        (setf *csv*
              ;; Skip the first header line
              (rest
               (cl-csv:read-csv maybe-pathname))))
      (v:info :cc-csv "Using previously loaded CSV data of length ~d~%" (length *csv*))))

(defun load-card-sets ()
  "Cache CARD-SET info to match up from PRODUCT-NAME."
  (unless *card-sets*
    (with-datafly-connection (db)
      (with-datafly-transaction
        (setf *card-sets*
              (datafly:retrieve-all
               (sxql:select (:code :passcode)
                 (sxql:from :ygo_set))))))))

;; TODO update this code with the new YGOProDeck stuff
(defun normalize-product-name (name)
  "Given a PRODUCT-NAME, apply the extracted data from the string: Name,
Set Code, Rarity, and Edition. Return a property list that can be
passed to SXQL:SET=."
  (let ((normalized-name name))
    ;; XXX EW
    ;; Replace things like "Rare-" and "Common-" With "Rare" and "Common"
    (setf normalized-name (cl-ppcre:regex-replace "(\\w+)- " normalized-name "\\1"))
    ;; A few single cards is borked
    (setf normalized-name (cl-ppcre:regex-replace "MRD-EN134 - Unlimited" normalized-name "MRD-EN134 - Common - Unlimited"))
    (setf normalized-name (cl-ppcre:regex-replace "SXG3-ENE05" normalized-name "SGX3-ENE05"))
    (setf normalized-name (cl-ppcre:regex-replace "wCPP-EN002" normalized-name "WCPP-EN002"))
    ;; This set was borknd
    ;; Maybe not, maybe it just doesn't have a Limited/Unlimited status?
    ;; (setf normalized-name (cl-ppcre:regex-replace "TKN4-([^ ]+) - Super Rare" normalized-name "TKN4-\\1 - Super Rare - Unlimited"))
    ;; Replace things like Rare 1st Edition with Rare - 1st Edition
    (setf normalized-name (cl-ppcre:regex-replace "(Common|Rare) (1st|Limited|Unlimited)" normalized-name "\\1 - \\2"))
    (let ((parts (reverse
                  (cl-ppcre:split " \\s?[-–] \\s?" normalized-name))))
      (case (length parts)
        (1 `(:name ,(first parts)))
        (2 `(:name ,(format nil "~{~A~^ - ~}" parts)))
        (3 `(:name ,(first parts)
             :code ,(second parts)
             :rarity ,(third parts)))
        (4 `(:name ,(first parts)
             :code ,(second parts)
             :rarity ,(third parts)
             :edition ,(fourth parts)))
        (5 `(:name ,(format nil "~{~A~^ - ~}" (subseq parts 0 2))
             :code ,(third parts)
             :rarity ,(fourth parts)
             :edition ,(fifth parts)))
        (6 `(:name ,(format nil "~{~A~^ - ~}" (subseq parts 0 3))
             :code ,(fourth parts)
             :rarity ,(fifth parts)
             :edition ,(sixth parts)))
        (otherwise (format t "Skipping data: ~a~%" name))))))

;; (defparameter *db-variants*
;;   (let ((ht (make-hash-table)))
;;     (loop for variant in (select-dao 'variant) do
;;       (setf (gethash (variant-name-of variant) ht) variant))
;;     ht))

;; TODO clean up the field names to auto generate
;;
;; TODO I added some additional fields, opt-qty reserved-qty condition language
;; I guess we will need one of each in the db...
(defun csv-import-cc (csv)
  "Import the perfect data from Crystal Commerce. Do nothing with it but import it into the table."
  (load-csv csv)

  (v:info :cc-csv "CSV Loaded")
  ;; (load-card-sets)
  ;; (format t "Card sets loaded from DB~&")
  
  (let ((csv-items (seed-csv-items)))
    (pre-seed-db-items csv-items))

  (v:info :cc-csv "done.~&"))

(defun seed-csv-items ()
  (let ((csv-items '()))
    (with-connection (db)
      (with-transaction
        (dolist (row *csv*)
          (let ((row-plist (mapcan #'list *cc-csv-header-fields* row)))
            (push (or (mito:find-dao 'cc-item :product-name (getf row-plist :product-name))
                      (apply #'make-instance 'cc-item row-plist))
                  csv-items)))))
    (v:info :cc-csv "CSV Items: ~d" (length csv-items))

    (unless (mito:dao-synced (car csv-items))
      (do-grouped-insert csv-items))))

(defun pre-seed-db-items (cc-items)
  (let ((db-items '()))
    (with-connection (db)
      (with-transaction
        (dolist (cc-item cc-items)
          (dolist (variant (select-dao 'variant))
            (push (or (mito:find-dao 'ygo-cc-item :item-id (mito:object-id cc-item)
                                                  :variant-id (mito:object-id variant))
                      (make-instance 'ygo-cc-item
                                     :item cc-item
                                     :variant variant))
                  db-items)))))
    (v:info :cc-csv "DB Items: ~d" (length db-items))

    (unless (mito:dao-synced (car db-items))
      (do-grouped-insert db-items))))
