(defpackage org.lispbuilds.nix/database/sqlite
  (:use :cl)
  (:import-from :str)
  (:import-from :sqlite)
  (:import-from :alexandria :read-file-into-string)
  (:import-from :alexandria-2 :line-up-first)
  (:import-from :arrow-macros :->>)
  (:import-from
   :org.lispbuilds.nix/util
   :replace-regexes)
  (:import-from
   :org.lispbuilds.nix/nix
   :nix-eval
   :nixify-symbol
   :system-master
   :make-pname
   :*nix-attrs-depth*)
  (:import-from
   :org.lispbuilds.nix/api
   :database->nix-expression)
  (:export :sqlite-database :init-db)
  (:local-nicknames
   (:hydra :org.lispbuilds.nix/hydra)
   (:json :com.inuoe.jzon)))

(in-package org.lispbuilds.nix/database/sqlite)

(defclass sqlite-database ()
  ((url :initarg :url
        :reader database-url
        :initform (error "url required"))
   (init-file :initarg :init-file
              :reader init-file
              :initform (error "init file required"))))

(defun init-db (db init-file)
  (let ((statements (->> (read-file-into-string init-file)
                         (replace-regexes '(".*--.*") '(""))
                         (substitute #\Space #\Newline)
                         (str:collapse-whitespaces)
                         (str:split #\;)
                         (mapcar #'str:trim)
                         (remove-if #'str:emptyp))))
    (sqlite:with-transaction db
      (dolist (s statements)
        (sqlite:execute-non-query db s)))))


;; Writing Nix

(defparameter prelude "
# This file was auto-generated by nix-quicklisp.lisp

{ runCommand, pkgs, lib, fetchzip, build-asdf-system, ... }:

let

 inherit (builtins) getAttr;

# Ensures that every non-slashy `system` exists in a unique .asd file.
# (Think cl-async-base being declared in cl-async.asd upstream)
#
# This is required because we're building and loading a system called
# `system`, not `asd`, so otherwise `system` would not be loadable
# without building and loading `asd` first.
#
 createAsd = { url, sha256, asd, system }:
   let
     src = fetchzip { inherit url sha256; };
   in
     if asd == system
     then src
     else runCommand \"source\" {} ''
       mkdir -pv $out
       cp -r ${src}/* $out
       find $out -name \"${asd}.asd\" | while read f; do mv -fv $f $(dirname $f)/${system}.asd || true; done
     '';
in lib.makeScope pkgs.newScope (self: {")

;; Random compilation errors
(defparameter +broken-packages+
  (list
   ;; no dispatch function defined for #\t
   "hu.dwim.logger"
   "hu.dwim.serializer"
   "hu.dwim.quasi-quote"
   ;; Tries to write in $HOME
   "ubiquitous"
   ;; Upstream bad packaging, multiple systems in clml.blas.asd
   "clml.blas.hompack"
   ;; Fails on SBCL due to heap exhaustion
   "magicl"
   ;; Missing dependency on c2ffi cffi extension
   "hu.dwim.zlib"
   ;; These require libRmath.so, but I don't know where to get it from
   "cl-random"
   "cl-random-tests"
   ))

(defmethod database->nix-expression ((database sqlite-database) outfile)
  (sqlite:with-open-database (db (database-url database))
    (with-open-file (f outfile
                       :direction :output
                       :if-exists :supersede)

      ;; Fix known problematic packages before dumping the nix file.
      (sqlite:execute-non-query db
       "create temp table fixed_systems as select * from system_view")

      (sqlite:execute-non-query db
       "alter table fixed_systems add column systems")

      (sqlite:execute-non-query db
       "update fixed_systems set systems = json_array(name)")

      (sqlite:execute-non-query db
       "alter table fixed_systems add column asds")

      (sqlite:execute-non-query db
       "update fixed_systems set asds = json_array(name)")

      (sqlite:execute-non-query db
       "delete from fixed_systems where name in ('asdf', 'uiop')")

      (sqlite:execute-non-query db
       "delete from fixed_systems where instr(name, '/')")

      (format f prelude)

      (dolist (p (sqlite:execute-to-list db "select * from fixed_systems"))
        (destructuring-bind (name version asd url sha256 deps systems asds) p
          (format f "~%  ")
          (let ((*nix-attrs-depth* 1))
            (format
             f
             "~a = ~a;"
             (nix-eval `(:symbol ,name))
             (nix-eval
              `(:funcall
                "build-asdf-system"
                (:attrs
                 ("pname" (:string ,(make-pname name)))
                 ("version" (:string ,version))
                 ("asds" (:list
                          ,@(mapcar (lambda (asd)
                                      `(:string ,(system-master asd)))
                                    (coerce (json:parse asds) 'list))))
                 ("src" (:funcall
                         "createAsd"
                         (:attrs
                          ("url" (:string ,url))
                          ("sha256" (:string ,sha256))
                          ("system" (:string ,(system-master name)))
                          ("asd" (:string ,asd)))))
                 ("systems" (:list
                             ,@(mapcar (lambda (sys)
                                         `(:string ,sys))
                                       (coerce (json:parse systems) 'list))))
                 ("lispLibs" (:list
                              ,@(mapcar (lambda (dep)
                                          `(:funcall
                                            "getAttr"
                                            (:string ,(nixify-symbol dep))
                                            (:symbol "self")))
                                        (line-up-first
                                         (str:split-omit-nulls #\, deps)
                                         (set-difference '("asdf" "uiop") :test #'string=)
                                         (sort #'string<)))))
                 ("meta" (:attrs
                          ,@(when (or (find #\/ name)
                                      (find name +broken-packages+ :test #'string=))
                              '(("broken" (:symbol "true"))))
                          ,@(unless (find name hydra:+allowlist+ :test #'string=)
                              '(("hydraPlatforms" (:list)))))))))))))
      (format f "~%})~%"))))
