(use-modules (json)
             (ice-9 match)
             (ice-9 format)
             (ice-9 iconv)
             (ice-9 receive)
             (ice-9 string-fun)
             (ice-9 exceptions)
             (srfi srfi-1)
             (srfi srfi-11)
             (srfi srfi-19)
             (srfi srfi-26)
             (rnrs io ports)
             (rnrs bytevectors)
             (web http)
             (web client)
             (web request)
             (web response)
             (web uri)
             (fibers web server)
             (gn cache memoize)
             (web gn-uri)
             (gn db sparql)
             (gn data species)
             (gn data group)
             (web sxml)
             (web view view)
             (web view doc)
             (web view markdown))

(define +current-repo-path+
  (getenv "CURRENT_REPO_PATH"))

(define +cgit-repo-path+
  (getenv "CGIT_REPO_PATH"))

(define +info+
  `(("name" . "GeneNetwork REST API") ("version" . ,get-version)
    ("comment" . "This is the official REST API for the GeneNetwork service hosted at https://genenetwork.org/")
    ("license"
     ("source code (unless otherwise specified)" . "Affero GNU Public License 3.0 (AGPL3)")
     ("data (unless otherwise specified)" . "Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)"))
    ("note" . "This is work in progress (WIP). Note that the final base URL will change! The temporary prefix is:")
    ("prefix" . ,(prefix))
    ("links" ("species" . ,(mk-meta "species")))))

(define +info-meta+
  `(("doc" ,(mk-html "info"))
    ("API" ((,(mk-url "species")) . "Get a list of all species")
     ((,(mk-url "mouse")) . "Get information on mouse")
     ((,(mk-url "datasets")) . "Get a list of datasets"))))

(define (get-id-data id)
  "Get data based on identifier id. If it is a taxon return the taxon data,
otherwise search for set/group data"
  (let ((taxoninfo (get-expanded-taxon-data id)))
    (if taxoninfo taxoninfo
        (cdr (get-group-data id)))))

(define (not-found2 request)
  (values (build-response #:code 404)
          (string-append "Resource X not found: "
                         (uri->string (request-uri request)))))

(define (not-found uri)
  (list (build-response #:code 404)
        (string-append "Resource not found: "
                       (uri->string uri))))

(define file-mime-types
  '(("css" text/css)
    ("js" text/javascript)
    ("svg" image/svg+xml)
    ("png" image/png)
    ("gif" image/gif)
    ("jpg" image/jpg)
    ("woff" application/font-woff)
    ("ttf" application/octet-stream)
    ("map" text/json)
    ("html" text/html)))

(define (file-extension file-name)
  (last (string-split file-name #\.)))

(define* (render-static-image file-name
                              #:key (extra-headers '()))
  (let* ((stat (stat file-name #f))
         (modified (and stat
                        (make-time time-utc 0
                                   (stat:mtime stat)))))
    (list `((content-type . ,(assoc-ref file-mime-types
				        (file-extension file-name)))
            (last-modified . ,(time-utc->date modified)))
          (call-with-input-file file-name
            get-bytevector-all))))

(define* (render-static-file path
                             #:optional rec
                             #:key (extra-headers '()))
  (let* ((stat (stat path #f))
         (modified (and stat
                        (make-time time-utc 0
                                   (stat:mtime stat)))))
    (list `((content-type . ,(assoc-ref file-mime-types
				        (file-extension path)))
            (last-modified . ,(time-utc->date modified)))
          (call-with-input-file path
            get-bytevector-all))))

(define* (render-doc path
                     page
                     #:optional rec
                     #:key (extra-headers '()))
  (list (append extra-headers
                '((content-type text/html)))
        (lambda (port)
          (sxml->html (view-doc path page rec) port))))

(define* (render-brand path
                       #:optional rec
                       #:key (extra-headers '()))
  (list (append extra-headers
                '((content-type text/html)))
        (lambda (port)
          (sxml->html (view-brand path) port))))

(define (render-json json)
  (list '((content-type application/json))
        (lambda (port)
          (scm->json json port))))

(define (render-json-string2 json)
  (list '((content-type text/plain))
        (lambda (port)
          (format port "~a" "foo"))))

(define (build-json-response status-code json)
  (list (build-response #:code status-code
                        #:headers `((content-type application/json)))
        (lambda (port)
          (scm->json json port))))

(define (decode-request-json body)
  (if (not body)
      '()
      (json-string->scm (utf8->string body))))

(define (decode-query-component component)
  (let* ((index (string-index component #\=))
         (key (if index
                  (substring component 0 index) component))
         (value (if index
                    (substring component
                               (1+ index)) "")))
    (cons (string->symbol (uri-decode key))
          (uri-decode value))))

(define (edit-file-handler repo request)
  (catch 'file-error
         (lambda ()
           (let* ((query (uri-query (request-uri request)))
                  (params (if (not query)
                              '()
                              (map decode-query-component
                                   (string-split query #\&))))
                  (query-path (assoc-ref params
                                         'file_path)))
             (if query-path
                 (build-json-response 200
                                      (fetch-file repo query-path))
                 (throw 'file-error
                        "Please provide a valid file path in the query"))))
         (lambda (key . args)
           (let ((msg (car args)))
             (build-json-response 400
                                  `(("error" . ,key)
                                    ("msg" . ,msg)))))))

(define (invalid-data? data target)
  (if (string? (assoc-ref data target))
      (if (string-null? (assoc-ref data target))
          (throw 'system-error
                 (format #f "Value for Key *** ~a  *** Cannot be Empty" target))
          (assoc-ref data target))
      (throw 'system-error
             (format #f "The Key  *** ~a *** is missing in your  Json Data"
                     target))))

(define (commit-file-handler repo request body)
  (catch 'system-error
         (lambda ()
	   (let* ((post-data (decode-request-json body))
		  (_ (for-each (lambda (target)
				 (invalid-data? post-data target))
			       '("filename" "content" "username" "email"
				 "prev_commit")))
		  (file-name (assoc-ref post-data "filename"))
		  (content (assoc-ref post-data "content"))
		  (username (assoc-ref post-data "username"))
		  (email (assoc-ref post-data "email"))
		  (commit-message (assoc-ref post-data "commit_message"))
		  (prev-commit (assoc-ref post-data "prev_commit")))
	     (build-json-response 200
				  ((lambda ()
				     (let ((message
					    (commit-file +current-repo-path+
							 file-name
							 content
							 commit-message
							 username
							 email
							 prev-commit)))
				       (git-invoke +current-repo-path+ "push" +cgit-repo-path+)
				       message))))))
         (lambda (key . args)
           (let ((msg (car args)))
             (build-json-response 400
                                  `(("error" . ,key)
                                    ("msg" . ,msg)))))))

(define (controller request body)
  (match-lambda
    (('GET)
     (render-json +info+))
    (('GET "version")
     (render-json get-version))
    (('GET "css" fn)
     (render-static-file (string-append (dirname (current-filename)) "/css/" fn)))
    (('GET "map" fn)
     (render-static-file (string-append (dirname (current-filename)) "/css/" fn)))
    (('GET "static" "images" fn)
     (render-static-image (string-append (dirname (current-filename)) "/static/images/" fn)))
    (('GET "home" path)
     (render-brand path)) ; branding route for /home/aging, /home/msk etc
    (('GET "doc" "species.html")
     (render-doc "doc" "species.html"
                 (get-species-meta)))
    (('GET "doc" taxon)
     (match (string->list taxon)
       ((name ...
              #\.
              #\h
              #\t
              #\m
              #\l)
        (render-doc "doc" taxon
                    (get-expanded-taxon-meta (list->string name))))))
    (('GET "doc" path ... page)
     ;; serve documents from /doc/
     (render-doc path page))
    (('GET "species.json")
     (render-json (get-species-data)))
    (('GET "species.meta.json")
     (render-json (get-species-meta)))
    (('GET "species")
     (render-json (get-species-meta)))
    (('GET "edit")
     (edit-file-handler +current-repo-path+ request))
    (('POST "commit")
     (commit-file-handler +current-repo-path+ request body))
    (('GET id)
     (let ((names (get-species-shortnames (get-expanded-species))))
       (match (string->list id)
         ((name ...
                #\.
                #\m
                #\e
                #\t
                #\a
                #\.
                #\j
                #\s
                #\o
                #\n)
          (render-json (get-expanded-taxon-meta (list->string name))))
         ((name ...
                #\.
                #\j
                #\s
                #\o
                #\n)
          (render-json (get-id-data (list->string name))))
         (rest (render-json "NOP")))))
    (_ (not-found (request-uri request)))))

(define (request-path-components request)
  (split-and-decode-uri-path (uri-path (request-uri request))))

(define (handler request body)
  (format #t "~a ~a\n"
          (request-method request)
          (uri-path (request-uri request)))
  (apply values
         ((controller request body)
          (cons (request-method request)
                (request-path-components request)))))

(define (start-web-server address port)
  (format (current-error-port)
          "GN REST API web server listening on http://~a:~a/~%" address port)
  ;; Wrap handler in another function to support live hacking via the
  ;; REPL. If handler is passed as is and is then redefined via the
  ;; REPL, the web server will still be using the old handler. The
  ;; only way to update the handler reference held by the web server
  ;; would be to restart the web server.
  (run-server (cut handler <> <>)
              #:addr (inet-pton AF_INET address)
              #:port port))

(define (main args)
  (write (string-append "Starting Guile REST API " get-version " server!"))
  (write args)
  (newline)
  (let ((listen (inexact->exact (string->number (car (cdr args))))))
    (display `("listening on" ,listen))
    (start-web-server "127.0.0.1" listen)))