#lang racket
(require racket/file
         "diff.rkt"
         "scm.rkt"
         "list-count.rkt"
         "notify.rkt"
         "cache.rkt"
         "dirstruct.rkt"
         "status.rkt"
         "metadata.rkt"
         "path-utils.rkt"
         "rendering.rkt")
(provide (all-from-out "rendering.rkt"))

; Email
(require net/sendmail
         "formats.rkt")

(define list@
  (match-lambda
    [(and c (cons x y))
     (if (lc-zero? x)
         empty
         (list c))]))

(define (list-limit l n)
  (for/list ([e (in-list l)]
             [i (in-range n)])
    e))

(define responsible-ht-id->str
  #hasheq([timeout . "Timeout"]
          [unclean . "Unclean Exit"]
          [stderr . "STDERR Output"]
          [changes . "Changes"]))
(define responsible-ht-severity
  '(timeout unclean stderr changes))
(define (rev->responsible-ht rev)
  (define log-dir (revision-log-dir rev))
  (define top-analyze
    (parameterize ([cache/file-mode 'no-cache]
                   [current-rev rev])
      (dir-rendering log-dir)))
  (rendering->responsible-ht rev top-analyze))

(define (rendering->responsible-ht rev top-analyze)
  (match-define
   (struct rendering (_ _ _ timeout unclean stderr _ changes))
   top-analyze)
  (statuses->responsible-ht rev timeout unclean stderr changes))

(define (statuses->responsible-ht rev timeout unclean stderr changes)
  (parameterize ([current-rev rev])
    (define log-dir (revision-log-dir rev))
    (define base-path 
      (rebase-path log-dir "/"))
    
    (define responsible->problems (make-hash))
    (for ([lc (in-list (list timeout unclean stderr changes))]
          [id (in-list responsible-ht-severity)])
      (for ([pp (in-list (lc->list lc))])
        (define p (bytes->string/utf-8 pp))
        (define bp (base-path p))
        (for ([responsible 
               (in-list
                (rendering-responsibles (log-rendering p)))])
          (hash-update!
           (hash-ref! responsible->problems responsible make-hasheq)
           id
           (curry list* bp)
           empty))))
    
    responsible->problems))

(define (2hash-copy ht)
  (define 2ht (make-hash))
  (for ([(r ht) (in-hash ht)])
    (hash-set! 2ht r (hash-copy ht)))
  2ht)
(define (responsible-ht-difference old new)
  (let ([ht (2hash-copy new)])
    (for ([(r rht) (in-hash old)])
      (define nrht (hash-ref! ht r make-hash))
      (for ([(id ps) (in-hash rht)])
        (hash-update! nrht id
                      (curry remove* ps)
                      empty)
        (when (zero? (length (hash-ref nrht id)))
          (hash-remove! nrht id)))
      (when (zero? (hash-count nrht))
        (hash-remove! ht r)))
    ht))

(define responsible-ht/c
  (hash/c string? (hash/c symbol? (listof path?))))

(define (responsible-ht->status-ht diff)
  (for/hash ([id (in-list responsible-ht-severity)])
    (define id-l
      (for*/list ([(_ ht) (in-hash diff)]
                  [f (in-list (hash-ref ht id empty))])
        f))
    (values id (remove-duplicates id-l))))

(provide/contract
 [rendering->responsible-ht
  (exact-positive-integer? rendering? . -> . responsible-ht/c)]
 [statuses->responsible-ht 
  (exact-positive-integer? list/count list/count list/count list/count . -> . responsible-ht/c)]
 [responsible-ht-severity (listof symbol?)]
 [responsible-ht-id->str (hash/c symbol? string?)]
 [responsible-ht-difference (responsible-ht/c responsible-ht/c . -> . responsible-ht/c)])  

(define ERROR-LIMIT 50)
(define (notify cur-rev 
                start end
                duration
                timeout unclean stderr changes)
  (define abs-dur (- end start))
  (define nums
    (map lc->number
         (list timeout unclean stderr changes)))
  (define totals
    (apply 
     format
     "(timeout ~a) (unclean ~a) (stderr ~a) (changes ~a)"
     (map number->string nums)))
  (define (path->url pth)
    (format "http://drdr.racket-lang.org/~a~a" cur-rev pth))
  (define responsible-ht
    (statuses->responsible-ht 
     cur-rev
     timeout
     unclean
     stderr
     changes))
  (define responsibles 
    (for/list ([(responsible ht) (in-hash responsible-ht)]
               #:when (ormap (curry hash-has-key? ht)
                             (take responsible-ht-severity 3)))
      responsible))
  (define committer
    (with-handlers ([exn:fail? (lambda (x) #f)])
      (scm-commit-author 
       (read-cache*
        (revision-commit-msg cur-rev)))))
  (define diff
    (with-handlers ([exn:fail? (lambda (x) #t)])
      (define old (rev->responsible-ht (previous-rev)))
      (responsible-ht-difference old responsible-ht)))
  (define include-committer?
    (and ; The committer can be found
     committer 
     ; There is a condition
     (not (empty? responsibles))
     ; It is different from before
     (hash? diff)
     (for*/or ([(r ht) (in-hash diff)]
               [(id ps) (in-hash ht)])
       (and 
        (for/or ([p (in-list ps)])
          ; XXX This squelch should be disabled if the committer changed this file
          ; XXX But even then it can lead to problems
          (not (path-random? (build-path (revision-trunk-dir cur-rev) (substring (path->string* p) 1)))))
        (not (symbol=? id 'changes))))))
  (define mail-recipients
    (remove-duplicates
     (append (if include-committer?
                 (list committer)
                 empty)
             responsibles)))
  
  ; Send messages to everyone...
  (unless (andmap zero? nums)
    (for ([r (in-list mail-recipients)])
      (send-mail-message 
       "drdr@racket-lang.org"
       (format "[DrDr] R~a ~a"
               cur-rev totals)
       (list (format "~a@racket-lang.org" r))
       empty empty
       (flatten
        (list (format "DrDr has finished building push #~a after ~a."
                      cur-rev
                      (format-duration-ms abs-dur))
              ""
              (format "http://drdr.racket-lang.org/~a/" 
                      cur-rev)
              ""
              (if (and include-committer? (equal? committer r))
                  (list
                   (format "Push #~a (which you did) contained a NEW condition that may need inspecting." cur-rev)
                   (let ([diff-smash (responsible-ht->status-ht diff)])
                     (for/list ([(id paths) (in-hash diff-smash)]
                                #:when (not (symbol=? id 'changes)))
                       (if (empty? paths)
                           empty
                           (list (format "  ~a" id)
                                 (for/list ([f (in-list paths)]
                                            [i (in-range ERROR-LIMIT)]
                                            #:when (not (path-random?
                                                         (build-path (revision-trunk-dir cur-rev)
                                                                     (substring (path->string* f) 1)))))
                                   (format "    ~a" (path->url f)))
                                 ""))))
                   "")
                  empty)
              (if (hash-has-key? responsible-ht r)
                  (list* "A file you are responsible for has a condition that may need inspecting."
                         (for/list ([(id files) (in-hash (hash-ref responsible-ht r))]
                                    #:when (not (symbol=? id 'changes)))
                           (list (format "  ~a:" id)
                                 (for/list ([f (in-list files)]
                                            [i (in-range ERROR-LIMIT)])
                                   (format "    ~a" (path->url f)))
                                 ""))
                         "")
                  empty))))))
  
  ; Send message to IRC
  (send-mail-message "drdr@racket-lang.org"
                     (format "http://drdr.racket-lang.org/~a/" 
                             cur-rev)
                     (list "eli+ircbot@eli.barzilay.org")
                     empty empty
                     (list* (format " (abs ~a) (sum ~a) ~a"
                                    (format-duration-ms abs-dur)
                                    (format-duration-ms duration)
                                    totals)
                            (if (empty? responsibles)
                                empty
                                (list (apply string-append (add-between responsibles " ")))))))
; End Email

(define (trunk-path pth)
  (define rev (current-rev))
  ((rebase-path (revision-log-dir rev) (revision-trunk-dir rev))
   pth))

(define (analyze-path pth dir?)
  (define rev (current-rev))
  (define log-dir (revision-log-dir rev))
  (define analyze-dir (revision-analyze-dir rev))
  (define the-analyze-path
    ((rebase-path log-dir analyze-dir) pth))
  (if dir?
      (build-path the-analyze-path "index.analyze")
      (path-add-suffix the-analyze-path ".analyze")))

(define (analyze-revision cur-rev)
  (cache/file/timestamp
   (build-path (revision-dir cur-rev) "analyzed")
   (lambda ()
     (match (analyze-logs cur-rev)
       [(struct rendering (start end duration timeout unclean stderr _ changes))
        (notify cur-rev 
                start end
                duration
                timeout unclean stderr changes)]
       [#f
        (void)])
     (safely-delete-directory (revision-trunk-dir cur-rev))
     (void))))

(define (analyze-logs rev)
  (define log-dir (revision-log-dir rev))
  (define analyze-dir (revision-analyze-dir rev))
  (make-directory* analyze-dir)
  (parameterize ([current-rev rev])
    (dir-rendering log-dir #:committer? #t)))

(define (log-rendering log-pth)
  ; XXX
  (if (or #t (file-exists? log-pth))
      (cache/file
       (analyze-path log-pth #f)
       (lambda ()
         #;(notify! "Analyzing log: ~S" log-pth)
         (match (read-cache log-pth)
           [(? eof-object?)
            #f]
           [(and log (struct status (start end command-line output-log)))
            (define dur (status-duration log))
            (define any-stderr? (ormap stderr? output-log))
            (define changed?
              (if (previous-rev)
                  (with-handlers ([exn:fail? 
                                   ; This #f means that new files are NOT considered changed
                                   (lambda (x) #f)])
                    (define prev-log-pth ((rebase-path (revision-log-dir (current-rev)) (revision-log-dir (previous-rev))) log-pth))
                    (log-different? output-log (status-output-log (read-cache prev-log-pth))))
                  #f))
            (define responsible 
              (or (path-responsible (trunk-path log-pth))
                  (and (regexp-match #rx"/planet/" (path->string* log-pth))
                       "jay")
                  ; XXX maybe mflatt, eli, or tewk
                  (and (regexp-match #rx"/src/" (path->string* log-pth))
                       "jay")
                  "nobody"))
            (define lc
              (list (path->bytes log-pth)))
            (make-rendering start end dur 
                            (if (timeout? log) lc empty)
                            (if (exit? log) 
                                (if (zero? (exit-code log)) empty lc)
                                empty)
                            (if any-stderr? lc empty)
                            responsible
                            (if changed? lc empty))])))
      #f))

(define (dir-rendering dir-pth 
                       #:committer? [committer? #f])
  ; XXX
  (if (or #t (directory-exists? dir-pth))
      (cache/file
       (analyze-path dir-pth #t)
       (lambda ()
         (notify! "Analyzing dir: ~S" dir-pth)
         (foldl (lambda (sub-pth acc)
                  (define pth (build-path dir-pth sub-pth))
                  (define directory? (directory-exists? pth))
                  (define (next-rendering)
                    (if directory?
                        (dir-rendering pth)
                        (log-rendering pth)))
                  (match (next-rendering)
                    [#f
                     acc]
                    [(and n (struct rendering (pth-start pth-end pth-dur pth-timeouts pth-unclean-exits pth-stderrs _pth-responsible pth-changed)))
                     (match acc
                       [#f n]
                       [(struct rendering (acc-start acc-end acc-dur acc-timeouts acc-unclean-exits acc-stderrs acc-responsible acc-changed))
                        (make-rendering (min pth-start acc-start)
                                        (max pth-end acc-end)
                                        (+ pth-dur acc-dur)
                                        (lc+ pth-timeouts acc-timeouts)
                                        (lc+ pth-unclean-exits acc-unclean-exits)
                                        (lc+ pth-stderrs acc-stderrs)
                                        acc-responsible
                                        (lc+ pth-changed acc-changed))])]))
                (make-rendering 
                 +inf.0 -inf.0 0
                 empty empty empty
                 
                 (or
                  (and committer? 
                       (with-handlers ([exn:fail? (lambda (x) #f)])
                         (scm-commit-author (read-cache (revision-commit-msg (current-rev))))))
                  (or (path-responsible (trunk-path dir-pth))
                      "nobody"))
                 
                 empty)
                (directory-list* dir-pth))))
      #f))

(provide/contract
 [analyze-revision (exact-nonnegative-integer? . -> . void)]
 [analyze-logs (exact-nonnegative-integer? . -> . void)]
 [log-rendering (path-string? . -> . (or/c rendering? false/c))]
 [dir-rendering (path-string? . -> . (or/c rendering? false/c))])
